/**
 * Front-end handler. Orchestrates client-side code generation: template-based file emission
 * (modules, translations, pivots), ts-morph reference wiring, and lockfile management.
 */
import { execFile } from 'node:child_process';
import * as fs from 'node:fs';
import { join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { promisify } from 'node:util';
import { IndentationText, Project, QuoteKind } from 'ts-morph';
import { NewLineKind } from 'typescript';
import { API_CONTAINER, APP_CONTAINER, DASHBOARD_CONTAINER, DASHBOARD_TRANSLATIONS, DETAIL_MODE, FRONT_PACKAGES, } from '../domain/config.js';
import { buildPivotSchema } from '../domain/model.js';
import { getManyToManyRelationshipProperties, getPivotFrontendExcludedFiles, getPivotFrontendKeptFiles, hasFormTabsProperties, hasI18nProperties, } from '../domain/property-filters.js';
import { loadSchema } from '../domain/schema-loader.js';
import { WidgetType } from '../domain/widget-type.js';
import { CodeWriter } from '../engine/code-writer/code-writer.js';
import { generateFromTemplate, } from '../engine/file-manager.js';
import { saveLockFiles } from '../engine/lock-file.js';
import { getBaseTemplatesDir, getTemplatesDir, } from '../engine/template-engine.js';
import { toKebabCase } from '../utils/string-utils.js';
import { buildOptions, copyDirRecursive, ensureDir, logger, } from './handler-utils.js';
export async function generateFrontModule(ctx) {
    const { schema } = ctx;
    const result = { lockFiles: [], originFiles: [] };
    // 1. Generate translation files
    await generateTranslationFiles(ctx, result);
    // 2. Generate front module files
    await generateModuleFiles(ctx, result);
    // 3. Generate module pivot files
    await generateModulePivotFiles(ctx, result);
    // 4. Wire ts-morph references
    await addReferences(ctx);
    // 5. Write lock file
    saveLockFiles(toKebabCase(schema.boundedContextName), schema.moduleName, result.lockFiles, ctx.scope);
    return result;
}
export async function newFrontApp(appName, _flags) {
    if (!appName)
        throw new Error('App name is required to create a front application');
    const src = join(getBaseTemplatesDir(), 'frontend', 'application');
    if (!fs.existsSync(src)) {
        throw new Error(`Front application template not found at ${src}`);
    }
    copyDirRecursive(src, appName, true);
}
const execFileAsync = promisify(execFile);
export async function addFrontPackage(packageFolder, flags) {
    if (!packageFolder)
        throw new Error('Package folder is required');
    // D2: resolve a single resolvedFrontendDir used for BOTH the copy destination
    // and the installer wiring. The CodeWriter methods join process.cwd() +
    // srcDirectory, so both must be aligned to the monorepo root.
    const monorepoRoot = process.cwd();
    const resolvedFrontendDir = join(monorepoRoot, 'frontend');
    // Guard: ensure we are running from the monorepo root by confirming that
    // both the frontend `src/` directory AND `tsconfig.json` exist. The tsconfig
    // check must come BEFORE any file copy so we don't leave a partial copy when
    // `new Project({ tsConfigFilePath })` would throw later in the installer.
    const expectedSrcDir = join(resolvedFrontendDir, 'src');
    if (!fs.existsSync(expectedSrcDir)) {
        throw new Error(`Expected a frontend workspace at "${resolvedFrontendDir}" (looking for "${expectedSrcDir}"), ` +
            `but that directory does not exist. ` +
            `Run "catalyst add frontend" from the monorepo root (the directory that contains the "frontend/" folder).`);
    }
    const expectedTsConfig = join(resolvedFrontendDir, 'tsconfig.json');
    if (!fs.existsSync(expectedTsConfig)) {
        throw new Error(`Expected a TypeScript configuration at "${expectedTsConfig}", ` +
            `but that file does not exist. ` +
            `Run "catalyst add frontend" from the monorepo root (the directory that contains the "frontend/" folder).`);
    }
    const definition = FRONT_PACKAGES.find((p) => p.packageFolder === packageFolder);
    const force = flags.force ?? false;
    const allSkipped = [];
    if (definition?.members && definition.members.length > 0) {
        // Meta-package: copy each member template tree into the frontend workspace dir.
        for (const member of definition.members) {
            const memberSrc = join(getBaseTemplatesDir(), 'frontend', 'packages', member);
            if (!fs.existsSync(memberSrc)) {
                throw new Error(`Front package member folder "${member}" not found at ${memberSrc}`);
            }
            const memberSkipped = copyDirRecursive(memberSrc, resolvedFrontendDir, force);
            allSkipped.push(...memberSkipped);
        }
    }
    else {
        // Single-package: copy the package template tree.
        const src = join(getBaseTemplatesDir(), 'frontend', 'packages', packageFolder);
        if (!fs.existsSync(src)) {
            throw new Error(`Front package folder "${packageFolder}" not found at ${src}`);
        }
        const singleSkipped = copyDirRecursive(src, resolvedFrontendDir, force);
        allSkipped.push(...singleSkipped);
    }
    const installerMessage = await runFrontPackageInstaller(packageFolder, resolvedFrontendDir);
    if (allSkipped.length > 0 && !force) {
        const skipNotice = `⚠ Skipped ${allSkipped.length} existing file(s). Re-run with --force to overwrite them.`;
        return installerMessage
            ? `${installerMessage}\n${skipNotice}`
            : skipNotice;
    }
    return installerMessage;
}
async function runFrontPackageInstaller(packageName, frontendDir) {
    const __dirname = fileURLToPath(new URL('.', import.meta.url));
    const installModulePath = join(__dirname, '..', 'packages', 'installers', 'front', `${packageName}.js`);
    if (!fs.existsSync(installModulePath))
        return;
    const mod = (await import(installModulePath));
    const installer = mod.default;
    const cwd = frontendDir;
    const tsConfigFilePath = join(cwd, 'tsconfig.json');
    const project = new Project({
        manipulationSettings: {
            indentationText: IndentationText.TwoSpaces,
            newLineKind: NewLineKind.LineFeed,
            quoteKind: QuoteKind.Single,
            usePrefixAndSuffixTextForRename: false,
            useTrailingCommas: false,
        },
        tsConfigFilePath,
    });
    const exec = async (cmd, args) => {
        await execFileAsync(cmd, args, { cwd });
    };
    const sourceFile = (...segments) => {
        const filePath = join(cwd, 'src', ...segments);
        return project.addSourceFileAtPath(filePath);
    };
    // D3: codeWriterFor factory — builds a CodeWriter bound to a given BC.
    // The three reuse methods read only boundedContextName + srcDirectory;
    // supply inert placeholders for schema-derived fields they never touch.
    const srcDirectory = 'frontend/src';
    const codeWriterFor = (boundedContextName) => new CodeWriter({
        aggregateName: '',
        apiDirectory: API_CONTAINER.replace('@', ''),
        appDirectory: APP_CONTAINER,
        boundedContextName,
        hasI18n: false,
        moduleName: '',
        moduleNames: '',
        srcDirectory,
    });
    return installer({ codeWriterFor, cwd, exec, project, sourceFile, srcDirectory });
}
async function generateTranslationFiles(ctx, result) {
    const { schema } = ctx;
    const bcKebab = toKebabCase(schema.boundedContextName);
    const outputBase = join(ctx.baseDir, DASHBOARD_TRANSLATIONS, bcKebab);
    ensureDir(ctx.baseDir, join(DASHBOARD_TRANSLATIONS, bcKebab));
    const templatesDir = getTemplatesDir();
    const translationsPath = join(templatesDir, 'frontend', 'module-translations');
    if (!fs.existsSync(translationsPath))
        return;
    const options = buildOptions(ctx.schema, ctx);
    await generateFromTemplate(logger, 'frontend/module-translations', outputBase, '', options, result);
}
async function generateModuleFiles(ctx, result) {
    const { schema } = ctx;
    // SPEC-15: validate grid-elements-manager targets BEFORE any file is written.
    // cliter dir is `process.cwd()/cliter/`, NOT `ctx.baseDir/cliter/` (baseDir is the output dir).
    validateGridElementsManagerTargets(schema, process.cwd());
    const bcKebab = toKebabCase(schema.boundedContextName);
    const outputBase = join(ctx.baseDir, 'src', DASHBOARD_CONTAINER, bcKebab);
    ensureDir(ctx.baseDir, join('src', DASHBOARD_CONTAINER, bcKebab));
    const options = buildOptions(ctx.schema, ctx);
    // Dialog mode: skip the detail component (`isFilePathAllowed` matches via `endsWith`,
    // so the bare filename is enough — the planner doesn't need the full target path).
    const modKebab = toKebabCase(schema.moduleName);
    if (schema.front?.detailMode === DETAIL_MODE.DIALOG) {
        options.excludedFiles = [
            ...(options.excludedFiles ?? []),
            `${modKebab}-detail.component.ts`,
        ];
        // SPEC-12: tabs are unsupported in dialog mode (space too limited).
        // The form template detects `detailMode === 'dialog'` and renders without `<hlm-tabs>`.
        if (hasFormTabsProperties(schema.aggregateProperties)) {
            console.warn(`[SPEC-12 warning] tabs declared on module ${schema.moduleName} with detailMode: dialog — tabs are ignored in dialog mode (space too limited). Render proceeds without tabs.`);
        }
        // grid-select-multiple-elements widget is unsupported in dialog mode: the
        // widget renders as a separate <section hlmCard> in the detail shell, which
        // is not emitted for dialog-mode aggregates. This warning makes the choice
        // visible to the developer.
        const gridMultiInDialog = (schema.aggregateProperties ?? []).some((p) => p.widget?.type === WidgetType.GRID_SELECT_MULTIPLE_ELEMENTS);
        if (gridMultiInDialog) {
            console.warn(`[grid-select-multiple-elements warning] Module ${schema.boundedContextName}/${schema.moduleName} declares widget.type: 'grid-select-multiple-elements' but uses detailMode: 'dialog'. The manager widget renders as a separate <section hlmCard> in the detail shell, which is not emitted for dialog-mode aggregates. The widget is skipped at codegen time.`);
        }
    }
    // SPEC-15: skip form-embed unless `front.embedSupport: true`. The template throws without
    // the many-to-one back-reference, so excluding it for non-embedable modules avoids errors.
    if (schema.front?.embedSupport !== true) {
        options.excludedFiles = [
            ...(options.excludedFiles ?? []),
            `${modKebab}-form-embed.component.ts`,
        ];
    }
    await generateFromTemplate(logger, 'frontend/module', outputBase, '', options, result);
}
async function generateModulePivotFiles(ctx, result) {
    const { schema } = ctx;
    if (!Array.isArray(schema.aggregateProperties))
        return;
    for (const property of getManyToManyRelationshipProperties(schema.aggregateProperties)) {
        if (!property.relationship?.pivot)
            continue;
        const { pivot } = property.relationship;
        const pivotBcKebab = toKebabCase(pivot.boundedContextName);
        const pivotOutputBase = join(ctx.baseDir, 'src', DASHBOARD_CONTAINER, pivotBcKebab);
        ensureDir(ctx.baseDir, join('src', DASHBOARD_CONTAINER, pivotBcKebab));
        const pivotOptions = buildOptions(buildPivotSchema(pivot), ctx);
        // Pivot aggregates are NEVER first-class frontend subjects in Aurora —
        // they exist as backend storage and a typed surface for the manager
        // widget. Skip emission of UI components (list / detail / form / columns
        // / barrel) but keep data-access (list-config / detail-config / graphql
        // / resolvers) which the manager widget consumes.
        const pivotModKebab = toKebabCase(pivot.moduleName);
        pivotOptions.excludedFiles = [
            ...(pivotOptions.excludedFiles ?? []),
            ...getPivotFrontendExcludedFiles(pivotModKebab),
        ];
        // eslint-disable-next-line no-await-in-loop
        await generateFromTemplate(logger, 'frontend/module', pivotOutputBase, '', pivotOptions, result);
        emitOrphanPivotFilesWarning(pivotBcKebab, pivotModKebab, pivotOutputBase);
    }
}
/**
 * Emit a `[ORPHAN PIVOT FILES]` warning listing pre-existing files at the
 * pivot's destination directory that are no longer generated by the codegen.
 * The codegen does NOT delete files automatically — the developer reviews the
 * list and runs `rm` manually.
 */
function emitOrphanPivotFilesWarning(pivotBcKebab, pivotModKebab, pivotOutputBase) {
    const pivotDir = join(pivotOutputBase, pivotModKebab);
    if (!fs.existsSync(pivotDir))
        return;
    const excluded = getPivotFrontendExcludedFiles(pivotModKebab);
    // Map the bare filenames in `excluded` to their actual locations under
    // `pivotDir` (features/* live in a subdirectory; columns.ts and index.ts
    // live at the pivot dir root).
    const orphanCandidates = excluded.map((file) => {
        if (file.endsWith('-list.component.ts') ||
            file.endsWith('-detail.component.ts') ||
            file.endsWith('-form.component.ts') ||
            file.endsWith('-form-embed.component.ts') ||
            file.endsWith('-list-actions-cell.component.ts')) {
            return { file, path: join(pivotDir, 'features', file) };
        }
        if (file.endsWith('.resolvers.ts')) {
            return { file, path: join(pivotDir, 'data-access', file) };
        }
        return { file, path: join(pivotDir, file) };
    });
    const orphans = orphanCandidates.filter(({ path }) => fs.existsSync(path));
    if (orphans.length === 0)
        return;
    const kept = getPivotFrontendKeptFiles(pivotModKebab);
    const keptPresent = kept.filter((relativePath) => fs.existsSync(join(pivotDir, relativePath)));
    console.warn(`[ORPHAN PIVOT FILES] ${pivotBcKebab}/${pivotModKebab}/ — these files are no longer generated by the codegen. The aggregate is a pivot (auto-derived from a many-to-many relationship) and is not represented as a standalone module. Safe to delete:`);
    for (const { file, path: filePath } of orphans) {
        const relativePath = filePath.startsWith(pivotDir + '/')
            ? filePath.slice(pivotDir.length + 1)
            : file;
        console.warn(`  ✗ ${relativePath}`);
    }
    for (const relativePath of keptPresent) {
        console.warn(`  ✓ ${relativePath} (KEEP — used by manager widget)`);
    }
}
function safeCodeWriterRun(label, fn) {
    try {
        fn();
    }
    catch (error) {
        logger.log(`[code-writer] ${label} skipped: ${error.message}`);
    }
}
async function addReferences(ctx) {
    const { schema } = ctx;
    const srcDir = join(ctx.baseDir, 'src');
    const codeWriter = new CodeWriter({
        aggregateName: schema.aggregateName,
        apiDirectory: APP_CONTAINER.replace('@', ''),
        appDirectory: join(APP_CONTAINER),
        boundedContextName: schema.boundedContextName.toLowerCase(),
        hasI18n: hasI18nProperties(schema.aggregateProperties),
        moduleName: schema.moduleName.toLowerCase(),
        moduleNames: schema.moduleNames.toLowerCase(),
        srcDirectory: srcDir,
    });
    safeCodeWriterRun('generateFrontRoutes', () => codeWriter.generateFrontRoutes());
    safeCodeWriterRun('declareFrontBoundedContext', () => codeWriter.declareFrontBoundedContext());
    safeCodeWriterRun('generateFrontNavigation', () => codeWriter.generateFrontNavigation(srcDir, schema.boundedContextName, schema.moduleName, schema.moduleNames, schema.front?.outlineIcon));
    safeCodeWriterRun('generateFrontIconsProviders', () => codeWriter.generateFrontIconsProviders(srcDir, schema.boundedContextName));
    safeCodeWriterRun('registerBcAggregatorInNavMain', () => codeWriter.registerBcAggregatorInNavMain(srcDir, schema.boundedContextName));
    safeCodeWriterRun('registerFrontNavigation', () => codeWriter.registerFrontNavigation());
    safeCodeWriterRun('generateFrontTranslations(en)', () => codeWriter.generateFrontTranslations(schema.aggregateProperties, 'en'));
    safeCodeWriterRun('generateFrontTranslations(es)', () => codeWriter.generateFrontTranslations(schema.aggregateProperties, 'es'));
    safeCodeWriterRun('generateFrontNavigationTranslation(en)', () => codeWriter.generateFrontNavigationTranslation('en'));
    safeCodeWriterRun('generateFrontNavigationTranslation(es)', () => codeWriter.generateFrontNavigationTranslation('es'));
    await codeWriter.formatSavedFiles();
}
/**
 * SPEC-15: walks `aggregateProperties` for any `widget.type: grid-elements-manager`. For each
 * match, loads the target child YAML and verifies `front.embedSupport: true` (and
 * `front.detailMode: dialog`). Throws with a corrective-action message otherwise.
 * Dialog-in-dialog (parent detailMode: dialog + any grid-elements-manager) only warns,
 * because the detail shell isn't emitted in dialog mode anyway.
 */
export function validateGridElementsManagerTargets(schema, cwd) {
    const offenders = (schema.aggregateProperties ?? []).filter((p) => p.widget?.type === WidgetType.GRID_ELEMENTS_MANAGER);
    if (offenders.length === 0)
        return;
    // Dialog-in-dialog: warn but allow regen — the detail shell is excluded anyway.
    if (schema.front?.detailMode === DETAIL_MODE.DIALOG) {
        console.warn(`[SPEC-15 warning] Module ${schema.boundedContextName}/${schema.moduleName} declares widget.type: grid-elements-manager but uses detailMode: dialog. Dialog-in-dialog rejected — the widget will be skipped at codegen time.`);
        return;
    }
    for (const prop of offenders) {
        const targetPath = prop.relationship?.modulePath;
        if (!targetPath) {
            throw new Error(`[SPEC-15 ERROR] ${schema.boundedContextName}/${schema.moduleName}.${prop.name} declares widget.type: grid-elements-manager but has no relationship.modulePath. The widget requires a relationship pointing to the child module.`);
        }
        const [childBc, childMod] = targetPath.split('/');
        if (!childBc || !childMod) {
            throw new Error(`[SPEC-15 ERROR] ${schema.boundedContextName}/${schema.moduleName}.${prop.name} references invalid target modulePath '${targetPath}'. Expected format '<bc>/<mod>'.`);
        }
        let childSchema;
        try {
            childSchema = loadSchema(childBc, childMod, cwd);
        }
        catch (error) {
            throw new Error(`[SPEC-15 ERROR] ${schema.boundedContextName}/${schema.moduleName}.${prop.name} declares widget.type: grid-elements-manager pointing to ${targetPath}, but the target schema could not be loaded: ${error.message}`);
        }
        if (childSchema.front?.embedSupport !== true) {
            throw new Error(`[SPEC-15 ERROR] ${schema.boundedContextName}/${schema.moduleName}.${prop.name} declares widget.type: grid-elements-manager but the target module ${targetPath} has not declared front.embedSupport: true. Add it to ${childMod}.aurora.yaml and regenerate the target module first (\`aurora generate front module --name=${targetPath}\`).`);
        }
        // SPEC-15: embed mode reuses the list-component's dialog machinery (the +New button opens
        // a dialog containing the form-embed). The child MUST also declare detailMode: dialog.
        if (childSchema.front?.detailMode !== DETAIL_MODE.DIALOG) {
            throw new Error(`[SPEC-15 ERROR] ${schema.boundedContextName}/${schema.moduleName}.${prop.name} targets ${targetPath}, which declares front.embedSupport: true but NOT front.detailMode: dialog. Embed mode reuses dialog wiring; add front.detailMode: dialog to ${childMod}.aurora.yaml and regenerate.`);
        }
    }
}
