/**
 * Back-end handler. Orchestrates server-side code generation: template emission (@app, @api,
 * postman, tests), ts-morph reference wiring, lockfile management, and YAML config persistence.
 */
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, BACK_PACKAGES } from '../domain/config.js';
import { buildPivotSchema } from '../domain/model.js';
import { getEnumProperties, getManyToManyRelationshipProperties, getValueObjectsProperties, hasI18nProperties, } from '../domain/property-filters.js';
import { saveSchema } from '../domain/schema-loader.js';
import { CodeWriter } from '../engine/code-writer/code-writer.js';
import { generateContents, 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, buildPropertyOptions, copyDirRecursive, ensureDir, logger, } from './handler-utils.js';
export async function generateBackModule(ctx, options = {}) {
    const { schema } = ctx;
    const result = { lockFiles: [], originFiles: [] };
    // 1. Generate @app module files
    await generateAppFiles(ctx, result);
    // 2. Generate @app pivot files
    await generateAppPivotFiles(ctx, result);
    // 3. Generate @app i18n files
    await generateAppI18nFiles(ctx, result);
    // 4. Generate @api files
    await generateApiFiles(ctx, result);
    // 5. Generate @api pivot files
    await generateApiPivotFiles(ctx, result);
    // 6. Generate additional API files
    await generateAdditionalApiFiles(ctx, result);
    // 7. Wire ts-morph references
    await addReferences(ctx);
    // 8. Wire pivot references
    await addPivotReferences(ctx);
    // 9. Optionally generate testing files
    if (options.hasTestingFiles) {
        await generateTestingFiles(ctx, result);
    }
    // 10. Generate postman files
    await generatePostmanFiles(ctx, result);
    // 11. Save YAML config
    saveSchema(schema);
    // 12. Write lock file
    saveLockFiles(toKebabCase(schema.boundedContextName), schema.moduleName, result.lockFiles, ctx.scope);
    return result;
}
export async function newBackApp(appName, _flags) {
    if (!appName)
        throw new Error('App name is required to create a back application');
    const src = join(getBaseTemplatesDir(), 'backend', 'application');
    if (!fs.existsSync(src)) {
        throw new Error(`Back application template not found at ${src}`);
    }
    copyDirRecursive(src, appName, true);
}
export async function addBackPackage(packageFolder, flags) {
    if (!packageFolder)
        throw new Error('Package folder is required');
    // O2 fix: resolve a single workspaceDir used for BOTH the copy destination
    // and the installer cwd.  The package template trees are laid out as
    // `packages/<pkg>/src/@api/...` (no `backend/` prefix), so copying into
    // workspaceDir lands `src/@api` next to the existing `src/` tree.
    const workspaceDir = 'backend';
    const monorepoRoot = process.cwd();
    const resolvedWorkspaceDir = join(monorepoRoot, workspaceDir);
    // Guard: ensure we are running from the monorepo root by confirming the
    // resolved backend workspace contains a `src/` directory.
    // Running `catalyst add backend` from inside the `backend/` workspace or
    // from an unrelated directory produces a misleading ts-morph error — this
    // clear message tells the operator where to run the command instead.
    const expectedSrcDir = join(resolvedWorkspaceDir, 'src');
    if (!fs.existsSync(expectedSrcDir)) {
        throw new Error(`Expected a backend workspace at "${resolvedWorkspaceDir}" (looking for "${expectedSrcDir}"), ` +
            `but that directory does not exist. ` +
            `Run "catalyst add backend" from the monorepo root (the directory that contains the "backend/" folder).`);
    }
    const definition = BACK_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 workspace dir.
        for (const member of definition.members) {
            const memberSrc = join(getBaseTemplatesDir(), 'backend', 'packages', member);
            if (!fs.existsSync(memberSrc)) {
                throw new Error(`Back package member folder "${member}" not found at ${memberSrc}`);
            }
            const memberSkipped = copyDirRecursive(memberSrc, resolvedWorkspaceDir, force);
            allSkipped.push(...memberSkipped);
        }
    }
    else {
        // Single-package: copy the package template tree (original behavior).
        const src = join(getBaseTemplatesDir(), 'backend', 'packages', packageFolder);
        if (!fs.existsSync(src)) {
            throw new Error(`Back package folder "${packageFolder}" not found at ${src}`);
        }
        const singleSkipped = copyDirRecursive(src, resolvedWorkspaceDir, force);
        allSkipped.push(...singleSkipped);
    }
    const installerMessage = await runPackageInstaller(packageFolder, workspaceDir, 'aurora-back');
    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;
}
const execFileAsync = promisify(execFile);
async function runPackageInstaller(packageName, workspaceDir, workspaceFilter) {
    const __dirname = fileURLToPath(new URL('.', import.meta.url));
    const installModulePath = join(__dirname, '..', 'packages', 'installers', `${packageName}.js`);
    if (!fs.existsSync(installModulePath))
        return;
    const mod = (await import(installModulePath));
    const installer = mod.default;
    const monorepoRoot = process.cwd();
    const cwd = join(monorepoRoot, workspaceDir);
    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 install = async (packages, opts) => {
        const devFlag = opts?.dev ? ['-D'] : [];
        await execFileAsync('pnpm', ['--filter', workspaceFilter, 'add', ...devFlag, ...packages], { cwd: monorepoRoot });
    };
    const sourceFile = (...segments) => {
        const filePath = join(cwd, 'src', ...segments);
        return project.addSourceFileAtPath(filePath);
    };
    return installer({ cwd, exec, install, project, sourceFile });
}
async function generateAppFiles(ctx, result) {
    const { schema } = ctx;
    if (schema.excludedFiles?.includes('src/@app/**'))
        return;
    const bcKebab = toKebabCase(schema.boundedContextName);
    const modKebab = toKebabCase(schema.moduleName);
    const outputBase = join(ctx.baseDir, 'src', APP_CONTAINER, bcKebab);
    ensureDir(ctx.baseDir, join('src', APP_CONTAINER, bcKebab));
    const options = buildOptions(ctx.schema, ctx);
    await generateFromTemplate(logger, 'backend/@app', outputBase, '', options, result);
    // Value objects per property
    const templatesDir = getTemplatesDir();
    for (const property of getValueObjectsProperties(schema.aggregateProperties)) {
        const typePath = join(templatesDir, 'backend', 'value-object', property.type);
        if (!fs.existsSync(typePath))
            continue;
        // eslint-disable-next-line no-await-in-loop
        await generateContents(logger, typePath, outputBase, join(modKebab, 'domain', 'value-objects'), buildPropertyOptions(options, property), result);
    }
    // Enum types per enum property
    for (const property of getEnumProperties(schema.aggregateProperties)) {
        const enumPath = join(templatesDir, 'backend', 'types', 'enum');
        if (!fs.existsSync(enumPath))
            continue;
        // eslint-disable-next-line no-await-in-loop
        await generateContents(logger, enumPath, outputBase, join(modKebab, 'domain', 'types'), buildPropertyOptions(options, property), result);
    }
}
async function generateAppPivotFiles(ctx, result) {
    const { schema } = ctx;
    if (!Array.isArray(schema.aggregateProperties))
        return;
    const templatesDir = getTemplatesDir();
    for (const property of getManyToManyRelationshipProperties(schema.aggregateProperties)) {
        if (!property.relationship?.pivot)
            continue;
        const { pivot } = property.relationship;
        const pivotBcKebab = toKebabCase(pivot.boundedContextName);
        const pivotModKebab = toKebabCase(pivot.moduleName);
        const pivotOutputBase = join(ctx.baseDir, 'src', APP_CONTAINER, pivotBcKebab);
        ensureDir(ctx.baseDir, join('src', APP_CONTAINER, pivotBcKebab));
        const pivotOpts = buildOptions(buildPivotSchema(pivot), ctx);
        // Pivots only need module-level files; targeting the module subdirectory directly
        // avoids duplicating BC-level seed/index for the same bounded context.
        // eslint-disable-next-line no-await-in-loop
        await generateFromTemplate(logger, 'backend/@app/__module_name__', join(pivotOutputBase, toKebabCase(pivot.moduleName)), '', pivotOpts, result);
        for (const pivotProp of getValueObjectsProperties(pivot.aggregateProperties)) {
            const typePath = join(templatesDir, 'backend', 'value-object', pivotProp.type);
            if (!fs.existsSync(typePath))
                continue;
            // eslint-disable-next-line no-await-in-loop
            await generateContents(logger, typePath, pivotOutputBase, join(pivotModKebab, 'domain', 'value-objects'), buildPropertyOptions(pivotOpts, pivotProp), result);
        }
        for (const pivotProp of getEnumProperties(pivot.aggregateProperties)) {
            const enumPath = join(templatesDir, 'backend', 'types', 'enum');
            if (!fs.existsSync(enumPath))
                continue;
            // eslint-disable-next-line no-await-in-loop
            await generateContents(logger, enumPath, pivotOutputBase, join(pivotModKebab, 'domain', 'types'), buildPropertyOptions(pivotOpts, pivotProp), result);
        }
    }
}
async function generateAppI18nFiles(ctx, result) {
    const { schema } = ctx;
    if (!hasI18nProperties(schema.aggregateProperties))
        return;
    const bcKebab = toKebabCase(schema.boundedContextName);
    const outputBase = join(ctx.baseDir, 'src', APP_CONTAINER, bcKebab);
    const templatesDir = getTemplatesDir();
    const i18nPath = join(templatesDir, 'backend', 'i18n-@app');
    if (!fs.existsSync(i18nPath))
        return;
    const options = buildOptions(ctx.schema, ctx);
    await generateContents(logger, i18nPath, outputBase, '', options, result);
}
async function generateApiFiles(ctx, result) {
    const { schema } = ctx;
    if (schema.excludedFiles?.includes('src/@api/**'))
        return;
    const bcKebab = toKebabCase(schema.boundedContextName);
    const outputBase = join(ctx.baseDir, 'src', API_CONTAINER, bcKebab);
    ensureDir(ctx.baseDir, join('src', API_CONTAINER, bcKebab));
    const options = buildOptions(ctx.schema, ctx);
    await generateFromTemplate(logger, 'backend/@api', outputBase, '', options, result);
}
async function generateApiPivotFiles(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', API_CONTAINER, pivotBcKebab);
        ensureDir(ctx.baseDir, join('src', API_CONTAINER, pivotBcKebab));
        const pivotOpts = buildOptions(buildPivotSchema(pivot), ctx);
        // eslint-disable-next-line no-await-in-loop
        await generateFromTemplate(logger, 'backend/@api', pivotOutputBase, '', pivotOpts, result);
    }
}
async function generateAdditionalApiFiles(ctx, result) {
    const { schema } = ctx;
    if (!schema.additionalApis?.length)
        return;
    const bcKebab = toKebabCase(schema.boundedContextName);
    const apiOutputBase = join(ctx.baseDir, 'src', API_CONTAINER, bcKebab);
    const appOutputBase = join(ctx.baseDir, 'src', APP_CONTAINER, bcKebab);
    const templatesDir = getTemplatesDir();
    const additionalApiPath = join(templatesDir, 'backend', 'additional-@api');
    const additionalAppPath = join(templatesDir, 'backend', 'additional-@app');
    const baseOpts = buildOptions(ctx.schema, ctx);
    for (const api of schema.additionalApis) {
        const options = {
            ...baseOpts,
            currentAdditionalApi: api,
        };
        // controllers & resolvers → @api
        if (fs.existsSync(additionalApiPath)) {
            // eslint-disable-next-line no-await-in-loop
            await generateContents(logger, additionalApiPath, apiOutputBase, '', options, result);
        }
        // handler → @app
        if (fs.existsSync(additionalAppPath)) {
            // eslint-disable-next-line no-await-in-loop
            await generateContents(logger, additionalAppPath, appOutputBase, '', options, result);
        }
    }
}
async function generateTestingFiles(ctx, result) {
    const templatesDir = getTemplatesDir();
    const testPath = join(templatesDir, 'backend', 'test');
    if (!fs.existsSync(testPath))
        return;
    const options = buildOptions(ctx.schema, ctx);
    await generateContents(logger, testPath, ctx.baseDir, 'test', options, result);
}
async function generatePostmanFiles(ctx, result) {
    const { schema } = ctx;
    const bcKebab = toKebabCase(schema.boundedContextName);
    const postmanDir = join(ctx.baseDir, 'postman', bcKebab);
    ensureDir(ctx.baseDir, join('postman', bcKebab));
    const options = buildOptions(ctx.schema, ctx);
    await generateFromTemplate(logger, 'backend/postman', postmanDir, '', options, result);
}
async function addReferences(ctx) {
    const { schema } = ctx;
    const srcDir = join(ctx.baseDir, 'src');
    const codeWriter = new CodeWriter({
        aggregateName: schema.aggregateName,
        apiDirectory: API_CONTAINER,
        appDirectory: join(APP_CONTAINER),
        boundedContextName: schema.boundedContextName.toLowerCase(),
        hasI18n: hasI18nProperties(schema.aggregateProperties),
        moduleName: schema.moduleName.toLowerCase(),
        moduleNames: schema.moduleNames.toLowerCase(),
        srcDirectory: srcDir,
    });
    codeWriter.generateBackAppBoundedContextReferences(schema.aggregateProperties, schema.excludedFiles);
    codeWriter.declareBackApplicationItemsInModule();
    codeWriter.declareBackBoundedContextModuleInApplicationModule();
    codeWriter.addSeedPermissions();
    await codeWriter.formatSavedFiles();
}
async function addPivotReferences(ctx) {
    const { schema } = ctx;
    if (!Array.isArray(schema.aggregateProperties))
        return;
    const srcDir = join(ctx.baseDir, 'src');
    for (const property of getManyToManyRelationshipProperties(schema.aggregateProperties)) {
        if (!property.relationship?.pivot) {
            throw new Error(`Pivot property is not defined in many-to-many relationship: ${property.name}`);
        }
        const { pivot } = property.relationship;
        const codeWriter = new CodeWriter({
            aggregateName: pivot.aggregateName,
            apiDirectory: API_CONTAINER,
            appDirectory: join(APP_CONTAINER),
            boundedContextName: pivot.boundedContextName.toLowerCase(),
            hasI18n: hasI18nProperties(pivot.aggregateProperties),
            moduleName: pivot.moduleName.toLowerCase(),
            moduleNames: pivot.moduleNames.toLowerCase(),
            srcDirectory: srcDir,
        });
        codeWriter.generateBackAppBoundedContextReferences(pivot.aggregateProperties, schema.excludedFiles);
        codeWriter.declareBackApplicationItemsInModule();
        codeWriter.declareBackBoundedContextModuleInApplicationModule();
        // eslint-disable-next-line no-await-in-loop
        await codeWriter.formatSavedFiles();
    }
}
