"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.resolveCustomAssetRoleContentType = resolveCustomAssetRoleContentType;
exports.normalizeAssetSpecSupportEntries = normalizeAssetSpecSupportEntries;
exports.loadAndValidateAssetSpecContract = loadAndValidateAssetSpecContract;
exports.validateCustomAssetFilesAgainstContract = validateCustomAssetFilesAgainstContract;
exports.normalizeAssetSpecVersionRef = normalizeAssetSpecVersionRef;
exports.validateAssetSpecName = validateAssetSpecName;
const node_fs_1 = require("node:fs");
const node_path_1 = require("node:path");
const ajv_1 = __importDefault(require("ajv"));
const semver_1 = require("semver");
const types_1 = require("@playdrop/types");
const LOCAL_EXTENSION_TO_MIME = {
    '.png': 'image/png',
    '.jpg': 'image/jpeg',
    '.jpeg': 'image/jpeg',
    '.webp': 'image/webp',
    '.gif': 'image/gif',
    '.aseprite': 'application/octet-stream',
    '.mp4': 'video/mp4',
    '.webm': 'video/webm',
    '.mp3': 'audio/mpeg',
    '.wav': 'audio/wav',
    '.ogg': 'audio/ogg',
    '.vox': 'application/octet-stream',
    '.gltf': 'model/gltf+json',
    '.glb': 'model/gltf-binary',
    '.json': 'application/json',
    '.md': 'text/markdown',
    '.txt': 'text/plain',
};
const ajv = new ajv_1.default({
    allErrors: true,
    strict: false,
    allowUnionTypes: true,
});
const validateContractMetaSchema = ajv.compile(types_1.ASSET_SPEC_CONTRACT_META_SCHEMA);
function formatAjvErrors(prefix) {
    return (validateContractMetaSchema.errors ?? []).map((issue) => {
        const path = issue.instancePath || issue.schemaPath || '/';
        return `${prefix} contract is invalid at ${path}: ${issue.message ?? 'schema validation failed'}.`;
    });
}
function createContractValidator() {
    return new ajv_1.default({
        allErrors: true,
        strict: false,
        allowUnionTypes: true,
    });
}
function resolveMimeTypeFromPath(filePath) {
    return LOCAL_EXTENSION_TO_MIME[(0, node_path_1.extname)(filePath).toLowerCase()] ?? 'application/octet-stream';
}
function resolveCustomAssetRoleContentType(filePath, roleContract) {
    const resolvedContentType = resolveMimeTypeFromPath(filePath);
    if (!roleContract) {
        return resolvedContentType;
    }
    const normalizedResolvedContentType = resolvedContentType.toLowerCase();
    for (const contentType of roleContract.contentTypes) {
        const normalizedContentType = contentType.trim().toLowerCase();
        if (normalizedContentType === normalizedResolvedContentType) {
            return contentType.trim();
        }
    }
    if ((0, node_path_1.extname)(filePath).toLowerCase() === '.json') {
        const jsonLikeContentType = roleContract.contentTypes.find((contentType) => (0, types_1.isJsonLikeAssetSpecContentType)(contentType));
        if (jsonLikeContentType) {
            return jsonLikeContentType.trim();
        }
    }
    return resolvedContentType;
}
// eslint-disable-next-line complexity
function normalizeAssetSpecSupportEntries(raw, errors, context) {
    if (raw === undefined || raw === null) {
        return [];
    }
    if (!Array.isArray(raw)) {
        errors.push(`${context} assetSpecSupport must be an array.`);
        return [];
    }
    const rows = [];
    for (let index = 0; index < raw.length; index += 1) {
        const entry = raw[index];
        if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
            errors.push(`${context} assetSpecSupport[${index}] must be an object.`);
            continue;
        }
        const assetSpec = typeof entry.assetSpec === 'string' ? entry.assetSpec.trim() : '';
        const parsedAssetSpec = (0, types_1.parseAssetSpecFamilyRef)(assetSpec);
        if (!assetSpec || !parsedAssetSpec) {
            errors.push(`${context} assetSpecSupport[${index}].assetSpec must be an asset spec family ref.`);
            continue;
        }
        const versionRange = typeof entry.versionRange === 'string' ? entry.versionRange.trim() : '';
        if (!versionRange || !(0, semver_1.validRange)(versionRange)) {
            errors.push(`${context} assetSpecSupport[${index}].versionRange must be a valid semver range.`);
            continue;
        }
        const capabilitiesRaw = Array.isArray(entry.capabilities) ? entry.capabilities : null;
        if (!capabilitiesRaw || capabilitiesRaw.length === 0) {
            errors.push(`${context} assetSpecSupport[${index}].capabilities must be a non-empty array.`);
            continue;
        }
        const capabilities = [];
        let capabilitiesValid = true;
        for (const capability of capabilitiesRaw) {
            if (!(0, types_1.isAssetSpecCapability)(capability)) {
                capabilitiesValid = false;
                break;
            }
            if (!capabilities.includes(capability)) {
                capabilities.push(capability);
            }
        }
        if (!capabilitiesValid) {
            errors.push(`${context} assetSpecSupport[${index}].capabilities contains an unsupported capability.`);
            continue;
        }
        rows.push({
            assetSpec,
            versionRange,
            capabilities,
        });
    }
    return rows;
}
// eslint-disable-next-line complexity
function loadAndValidateAssetSpecContract(catalogueDir, relativeContractPath, errors, context) {
    const contractPath = (0, node_path_1.resolve)(catalogueDir, relativeContractPath);
    let rawText;
    try {
        const stat = (0, node_fs_1.statSync)(contractPath);
        if (!stat.isFile()) {
            errors.push(`${context} contract must be a file: ${contractPath}.`);
            return null;
        }
        if (stat.size > types_1.ASSET_SPEC_CONTRACT_MAX_BYTES) {
            errors.push(`${context} contract exceeds the ${types_1.ASSET_SPEC_CONTRACT_MAX_BYTES} byte limit.`);
            return null;
        }
        rawText = (0, node_fs_1.readFileSync)(contractPath, 'utf8');
    }
    catch {
        errors.push(`${context} contract file not found at ${contractPath}.`);
        return null;
    }
    let parsed;
    try {
        parsed = JSON.parse(rawText);
    }
    catch (error) {
        errors.push(`${context} contract must be valid JSON (${error?.message ?? 'parse failed'}).`);
        return null;
    }
    if (!validateContractMetaSchema(parsed)) {
        errors.push(...formatAjvErrors(context));
        return null;
    }
    const contract = parsed;
    const roleNames = new Set();
    let hasContractError = false;
    for (const role of contract.roles) {
        const normalizedRoleName = role.name.trim().toLowerCase();
        if (roleNames.has(normalizedRoleName)) {
            errors.push(`${context} contract declares duplicate role "${role.name}".`);
            hasContractError = true;
            continue;
        }
        roleNames.add(normalizedRoleName);
        if (role.jsonSchema) {
            const allowsJsonSchema = role.contentTypes.some((contentType) => (0, types_1.isJsonLikeAssetSpecContentType)(contentType));
            if (!allowsJsonSchema) {
                errors.push(`${context} contract role "${role.name}" can only declare jsonSchema for JSON content types.`);
                hasContractError = true;
                continue;
            }
            const schemaAjv = createContractValidator();
            try {
                schemaAjv.compile(role.jsonSchema);
            }
            catch (error) {
                errors.push(`${context} contract role "${role.name}" jsonSchema is invalid (${error?.message ?? 'compile failed'}).`);
                hasContractError = true;
            }
        }
    }
    if (!roleNames.has(contract.primaryRole.trim().toLowerCase())) {
        errors.push(`${context} contract primaryRole "${contract.primaryRole}" must match one declared role.`);
        hasContractError = true;
    }
    const referenceNames = new Set();
    for (const reference of contract.references ?? []) {
        const normalizedReferenceName = reference.name.trim().toLowerCase();
        if (referenceNames.has(normalizedReferenceName)) {
            errors.push(`${context} contract declares duplicate reference "${reference.name}".`);
            hasContractError = true;
            continue;
        }
        referenceNames.add(normalizedReferenceName);
    }
    if (hasContractError) {
        return null;
    }
    return {
        contract,
        validationKind: contract.roles.some((role) => role.jsonSchema) ? 'JSON_SCHEMA' : 'NONE',
        contractPath,
    };
}
// eslint-disable-next-line complexity
function validateCustomAssetFilesAgainstContract(contract, filePaths, errors, context) {
    const contractRoles = new Map(contract.roles.map((role) => [role.name.trim().toLowerCase(), role]));
    const providedRoles = new Set();
    for (const [rawRole, filePath] of Object.entries(filePaths)) {
        const normalizedRole = rawRole.trim().toLowerCase();
        providedRoles.add(normalizedRole);
        const roleContract = contractRoles.get(normalizedRole);
        if (!roleContract) {
            errors.push(`${context} declares file role "${rawRole}" that is not present in the asset spec contract.`);
            continue;
        }
        const extension = (0, node_path_1.extname)(filePath).toLowerCase();
        if (roleContract.extensions?.length) {
            const allowedExtensions = roleContract.extensions.map((value) => value.trim().toLowerCase());
            if (!allowedExtensions.includes(extension)) {
                errors.push(`${context} role "${rawRole}" must use one of ${allowedExtensions.join(', ')}.`);
            }
        }
        const resolvedContentType = resolveCustomAssetRoleContentType(filePath, roleContract);
        const normalizedContentTypes = roleContract.contentTypes.map((value) => value.trim().toLowerCase());
        if (!normalizedContentTypes.includes(resolvedContentType.toLowerCase())) {
            errors.push(`${context} role "${rawRole}" must use one of ${normalizedContentTypes.join(', ')}.`);
        }
        if (roleContract.jsonSchema) {
            try {
                const rawText = (0, node_fs_1.readFileSync)(filePath, 'utf8');
                const parsedJson = JSON.parse(rawText);
                const schemaAjv = createContractValidator();
                const validateJson = schemaAjv.compile(roleContract.jsonSchema);
                if (!validateJson(parsedJson)) {
                    const firstIssue = validateJson.errors?.[0];
                    const issuePath = firstIssue?.instancePath || firstIssue?.schemaPath || '/';
                    errors.push(`${context} role "${rawRole}" JSON content failed schema validation at ${issuePath}: ${firstIssue?.message ?? 'schema validation failed'}.`);
                }
            }
            catch (error) {
                errors.push(`${context} role "${rawRole}" JSON content is invalid (${error?.message ?? 'parse failed'}).`);
            }
        }
    }
    for (const role of contract.roles) {
        if (role.required && !providedRoles.has(role.name.trim().toLowerCase())) {
            errors.push(`${context} must define required file role "${role.name}".`);
        }
    }
}
function normalizeAssetSpecVersionRef(raw, errors, context) {
    if (raw === undefined || raw === null) {
        return undefined;
    }
    if (typeof raw !== 'string' || raw.trim().length === 0) {
        errors.push(`${context} assetSpec must be an exact asset spec version ref.`);
        return undefined;
    }
    const parsed = (0, types_1.parseAssetSpecVersionRef)(raw.trim());
    if (!parsed) {
        errors.push(`${context} assetSpec must use canonical asset spec version ref format.`);
        return undefined;
    }
    return (0, types_1.formatAssetSpecVersionRef)(parsed);
}
function validateAssetSpecName(name, errors, context) {
    if (!(0, types_1.isValidAssetSpecName)(name)) {
        errors.push(`${context} name must use lowercase kebab-case.`);
        return false;
    }
    return true;
}
