/** Loads and validates module definition schemas from YAML files. */
import fs from 'node:fs';
import path from 'node:path';
import YAML from 'yaml';
import { DETAIL_MODE, GENDER, SCHEMA_DEFINITION_EXTENSION } from './config.js';
/**
 * Load a module definition schema from a YAML file.
 *
 * Expected path: `{cwd}/cliter/{boundedContext}/{moduleName}.aurora.yaml`
 */
export function loadSchema(boundedContextName, moduleName, cwd = process.cwd()) {
    const yamlPath = path.join(cwd, 'cliter', boundedContextName, moduleName + SCHEMA_DEFINITION_EXTENSION);
    if (!fs.existsSync(yamlPath)) {
        throw new Error(`Schema file not found: ${yamlPath}`);
    }
    const raw = fs.readFileSync(yamlPath, 'utf8');
    const parsed = YAML.parse(raw);
    validateSchema(parsed);
    return addDefaults(parsed);
}
export function saveSchema(schema, cwd = process.cwd()) {
    const dir = path.join(cwd, 'cliter', schema.boundedContextName);
    if (!fs.existsSync(dir)) {
        fs.mkdirSync(dir, { recursive: true });
    }
    const yamlStr = YAML.stringify(stripDefaults(schema), {
        lineWidth: 0,
    });
    fs.writeFileSync(path.join(dir, `${schema.moduleName}${SCHEMA_DEFINITION_EXTENSION}`), yamlStr, 'utf8');
}
export function parseBoundedContextModule(input) {
    const [boundedContextName, moduleName] = input.split('/');
    if (!boundedContextName || !moduleName) {
        throw new Error(`Invalid format "${input}". Expected: bounded-context/module`);
    }
    return { boundedContextName, moduleName };
}
function validateSchema(obj) {
    const required = [
        ['boundedContextName', 'string'],
        ['moduleName', 'string'],
        ['moduleNames', 'string'],
        ['aggregateName', 'string'],
        ['hasOAuth', 'boolean'],
        ['hasTenant', 'boolean'],
        ['hasAuditing', 'boolean'],
    ];
    for (const [field, expectedType] of required) {
        if (typeof obj[field] !== expectedType) {
            throw new TypeError(`YAML schema error: "${field}" must be a ${expectedType} (got ${typeof obj[field]})`);
        }
    }
    validateFrontBlock(obj);
}
function validateFrontBlock(obj) {
    const front = obj.front;
    if (front === undefined || front === null)
        return;
    if (typeof front !== 'object' || Array.isArray(front)) {
        throw new TypeError(`YAML schema error in "${String(obj.moduleName)}": "front" must be an object`);
    }
    const frontObj = front;
    const validDetailModes = [DETAIL_MODE.VIEW, DETAIL_MODE.DIALOG];
    if (frontObj.detailMode !== undefined &&
        !validDetailModes.includes(frontObj.detailMode)) {
        throw new TypeError(`YAML schema error in "${String(obj.moduleName)}": "front.detailMode" must be one of ${validDetailModes.map(v => `'${v}'`).join(' | ')} (got ${JSON.stringify(frontObj.detailMode)})`);
    }
}
function applyPropertyDefaults(properties) {
    for (const property of properties) {
        // UUID id properties default to length 36
        if (property.type === 'id' && property.length === undefined) {
            property.length = 36;
        }
        if (property.type === 'relationship' &&
            property.relationship?.type === 'many-to-many' &&
            Array.isArray(property.relationship.pivot?.aggregateProperties)) {
            applyPropertyDefaults(property.relationship.pivot.aggregateProperties);
        }
    }
}
function addDefaults(schema) {
    if (Array.isArray(schema.aggregateProperties)) {
        applyPropertyDefaults(schema.aggregateProperties);
    }
    return {
        ...schema,
        additionalApis: schema.additionalApis ?? [],
        excludedFiles: schema.excludedFiles ?? [],
        excludedOperations: schema.excludedOperations ?? [],
        front: {
            ...schema.front,
            detailMode: schema.front?.detailMode ?? DETAIL_MODE.VIEW,
            // Default `masculine` so generators can read `schema.front.gender` without a nullish
            // fallback. Stripped by `stripDefaults` before persisting.
            gender: schema.front?.gender ?? GENDER.MASCULINE,
        },
    };
}
function stripPropertyDefaults(properties) {
    for (const property of properties) {
        // Length 36 on `id` is implied — strip before persisting
        if (property.type === 'id' && property.length === 36) {
            delete property.length;
        }
        if (property.type === 'relationship' &&
            property.relationship?.type === 'many-to-many' &&
            Array.isArray(property.relationship.pivot?.aggregateProperties)) {
            stripPropertyDefaults(property.relationship.pivot.aggregateProperties);
        }
    }
}
function stripDefaults(schema) {
    const { ...rest } = schema;
    if (Array.isArray(rest.aggregateProperties)) {
        stripPropertyDefaults(rest.aggregateProperties);
    }
    // Remove empty arrays to keep YAML clean
    const result = rest;
    if (Array.isArray(result.additionalApis) && result.additionalApis.length === 0) {
        delete result.additionalApis;
    }
    if (Array.isArray(result.excludedFiles) && result.excludedFiles.length === 0) {
        delete result.excludedFiles;
    }
    if (Array.isArray(result.excludedOperations) && result.excludedOperations.length === 0) {
        delete result.excludedOperations;
    }
    // Strip implied `front.*` defaults; drop `front` entirely if empty so load → save round-trips
    // produce the same YAML for modules that never declared any front fields.
    if (result.front && typeof result.front === 'object') {
        const front = result.front;
        if (front.gender === GENDER.MASCULINE) {
            delete front.gender;
        }
        if (front.detailMode === DETAIL_MODE.VIEW) {
            delete front.detailMode;
        }
        if (Object.keys(front).length === 0) {
            delete result.front;
        }
    }
    return result;
}
