"use strict";
/**
* Migration: Reorganize Applications by Distribution Channel
*
* This migration reorganizes applications based on their `distributionChannel` property in `.unisphere`:
*
* Directory structure:
* - none (local-dev-playground) -> applications/local/
* - unisphere (server) -> applications/server/
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.default = update;
const devkit_1 = require("@nx/devkit");
const fs_1 = require("fs");
const path_1 = require("path");
/**
 * Get the subdirectory based on distribution channel
 */
function getSubdirectory(distributionChannel) {
    switch (distributionChannel) {
        case 'none':
            return 'local';
        case 'unisphere':
            return 'server';
        default:
            // Default to local for unknown channels
            return 'local';
    }
}
/**
 * Get the actual Nx project name from project.json
 */
function getProjectNameFromProjectJson(tree, projectPath) {
    const projectJsonPath = `${projectPath}/project.json`;
    if (!tree.exists(projectJsonPath)) {
        return null;
    }
    try {
        const projectJson = (0, devkit_1.readJson)(tree, projectJsonPath);
        return projectJson.name || null;
    }
    catch {
        return null;
    }
}
/**
 * Check if an application has already been migrated to the new subdirectory structure
 */
function isAlreadyMigrated(tree, name, distributionChannel) {
    const subdirectory = getSubdirectory(distributionChannel);
    const newPath = `unisphere/applications/${subdirectory}/${name}`;
    // Check if the new path exists and has a project.json
    return tree.exists(`${newPath}/project.json`);
}
/**
 * Discover all applications in the workspace
 */
function discoverApplications(tree) {
    if (!tree.exists('.unisphere')) {
        devkit_1.logger.warn('No .unisphere file found, skipping migration');
        return [];
    }
    const unisphereConfig = (0, devkit_1.readJson)(tree, '.unisphere');
    const applications = [];
    const applicationsConfig = unisphereConfig.elements?.applications || {};
    for (const [name, config] of Object.entries(applicationsConfig)) {
        const normalizedName = (0, devkit_1.names)(name).fileName;
        // Get sourceRoot from .unisphere config, or fall back to default path
        const sourceRoot = config.sourceRoot;
        const currentPath = sourceRoot || `unisphere/applications/${normalizedName}`;
        // Infer distributionChannel: use explicit property if present, otherwise
        // infer from current path (handles re-runs after upgrade-schema-to-2-0-0
        // has already removed distributionChannel).
        let distributionChannel = config.distributionChannel || 'none';
        if (!config.distributionChannel && currentPath.includes('/applications/server/')) {
            distributionChannel = 'unisphere';
        }
        else if (!config.distributionChannel && currentPath.includes('/applications/local/')) {
            distributionChannel = 'none';
        }
        // Calculate target path
        const subdirectory = getSubdirectory(distributionChannel);
        const targetPath = `unisphere/applications/${subdirectory}/${normalizedName}`;
        // Check if application is already at the target location (sourceRoot == targetPath)
        if (currentPath === targetPath) {
            devkit_1.logger.info(`  Application ${name} is already at target location (${targetPath}), skipping`);
            continue;
        }
        // Check if already migrated to new location (project.json exists at new location)
        if (isAlreadyMigrated(tree, normalizedName, distributionChannel)) {
            devkit_1.logger.info(`  Application ${name} already migrated to new location, skipping`);
            continue;
        }
        // Check if target path already exists with actual content (project.json)
        // If target exists but is empty (no project.json), clean it up and proceed
        if (tree.exists(targetPath)) {
            if (tree.exists(`${targetPath}/project.json`)) {
                devkit_1.logger.info(`  Application ${name} target path already exists with project.json (${targetPath}), skipping`);
                continue;
            }
            else {
                // Empty directory - clean it up so migration can proceed
                devkit_1.logger.info(`  Application ${name} target path exists but is empty, cleaning up: ${targetPath}`);
                tree.delete(targetPath);
            }
        }
        if (!tree.exists(currentPath)) {
            devkit_1.logger.warn(`Application ${name} not found at ${currentPath}, skipping`);
            continue;
        }
        // Read actual project name from project.json instead of assuming it
        const actualProjectName = getProjectNameFromProjectJson(tree, currentPath);
        if (!actualProjectName) {
            devkit_1.logger.warn(`Application ${name} has no project.json or missing name at ${currentPath}, skipping`);
            continue;
        }
        devkit_1.logger.info(`  Found application: ${normalizedName} (Nx project: ${actualProjectName})`);
        applications.push({
            name: normalizedName,
            distributionChannel: distributionChannel,
            currentPath,
            currentNxProjectName: actualProjectName,
        });
    }
    return applications;
}
/**
 * Calculate migration target for an application
 */
function calculateMigrationTarget(app) {
    const subdirectory = getSubdirectory(app.distributionChannel);
    const newPath = `unisphere/applications/${subdirectory}/${app.name}`;
    return {
        application: app,
        newPath,
        newNxProjectName: app.currentNxProjectName,
        subdirectory,
    };
}
/**
 * Validate migration preconditions
 */
function validateMigrationPreconditions(tree, targets) {
    // Check destination directories don't conflict
    const destinations = new Set();
    for (const target of targets) {
        if (destinations.has(target.newPath)) {
            throw new Error(`Duplicate destination path: ${target.newPath}`);
        }
        destinations.add(target.newPath);
        // Check destination doesn't already exist
        if (tree.exists(target.newPath)) {
            throw new Error(`Destination already exists: ${target.newPath}`);
        }
    }
    // Check all source applications exist
    for (const target of targets) {
        if (!tree.exists(target.application.currentPath)) {
            throw new Error(`Source application not found: ${target.application.currentPath}`);
        }
    }
}
/**
 * Update .unisphere configuration sourceRoot
 */
function updateUnisphereSourceRoot(tree, name, newPath) {
    const unisphereConfig = (0, devkit_1.readJson)(tree, '.unisphere');
    if (unisphereConfig.elements?.applications?.[name]) {
        const applicationConfig = unisphereConfig.elements.applications[name];
        applicationConfig.sourceRoot = newPath;
        (0, devkit_1.writeJson)(tree, '.unisphere', unisphereConfig);
    }
}
/**
 * Update package-lock.json paths
 */
function updatePackageLockPaths(tree, oldPath, newPath) {
    if (!tree.exists('package-lock.json')) {
        return;
    }
    const packageLock = (0, devkit_1.readJson)(tree, 'package-lock.json');
    let updated = false;
    if (packageLock.packages) {
        const newPackages = {};
        for (const [key, value] of Object.entries(packageLock.packages)) {
            if (key === oldPath) {
                newPackages[newPath] = value;
                updated = true;
            }
            else if (key.startsWith(oldPath + '/')) {
                // Handle nested node_modules paths if any
                newPackages[key.replace(oldPath, newPath)] = value;
                updated = true;
            }
            else {
                newPackages[key] = value;
            }
        }
        if (updated) {
            packageLock.packages = newPackages;
        }
    }
    // Update resolved paths if they exist
    if (packageLock.packages) {
        for (const value of Object.values(packageLock.packages)) {
            if (value && typeof value === 'object' && 'resolved' in value) {
                const pkg = value;
                if (pkg.resolved === oldPath) {
                    pkg.resolved = newPath;
                    updated = true;
                }
            }
        }
    }
    if (updated) {
        (0, devkit_1.writeJson)(tree, 'package-lock.json', packageLock);
        devkit_1.logger.info(`  Updated package-lock.json paths`);
    }
}
/**
 * Fix .eslintrc.json extends path after moving to subdirectory
 * Path changes from ../../../.eslintrc.json to ../../../../.eslintrc.json
 */
function fixEslintConfigPath(tree, applicationPath) {
    const eslintConfigPath = `${applicationPath}/.eslintrc.json`;
    if (!tree.exists(eslintConfigPath)) {
        return;
    }
    const eslintConfig = (0, devkit_1.readJson)(tree, eslintConfigPath);
    if (eslintConfig.extends && Array.isArray(eslintConfig.extends)) {
        const updatedExtends = eslintConfig.extends.map((ext) => {
            // Update the relative path to root .eslintrc.json
            if (ext === '../../../.eslintrc.json') {
                return '../../../../.eslintrc.json';
            }
            return ext;
        });
        eslintConfig.extends = updatedExtends;
        (0, devkit_1.writeJson)(tree, eslintConfigPath, eslintConfig);
        devkit_1.logger.info(`  Fixed .eslintrc.json extends path`);
    }
}
/**
 * Fix tsconfig.json extends path after moving to subdirectory
 * Path changes from ../../../tsconfig.base.json to ../../../../tsconfig.base.json
 */
function fixTsconfigJsonPath(tree, applicationPath) {
    const tsconfigPath = `${applicationPath}/tsconfig.json`;
    if (!tree.exists(tsconfigPath)) {
        return;
    }
    const tsconfig = (0, devkit_1.readJson)(tree, tsconfigPath);
    if (tsconfig.extends === '../../../tsconfig.base.json') {
        tsconfig.extends = '../../../../tsconfig.base.json';
        (0, devkit_1.writeJson)(tree, tsconfigPath, tsconfig);
        devkit_1.logger.info(`  Fixed tsconfig.json extends path`);
    }
}
/**
 * Fix jest.config.ts paths after moving to subdirectory
 * - preset path changes from ../../../jest.preset.js to ../../../../jest.preset.js
 * - coverageDirectory path needs extra level
 */
function fixJestConfigPath(tree, applicationPath, newPath) {
    const jestConfigPath = `${applicationPath}/jest.config.ts`;
    if (!tree.exists(jestConfigPath)) {
        return;
    }
    let content = tree.read(jestConfigPath, 'utf-8');
    if (!content) {
        return;
    }
    let updated = false;
    // Fix preset path
    if (content.includes("preset: '../../../jest.preset.js'")) {
        content = content.replace("preset: '../../../jest.preset.js'", "preset: '../../../../jest.preset.js'");
        updated = true;
    }
    // Fix coverageDirectory path - update to use the new path
    const coverageDirRegex = /coverageDirectory:\s*['"]\.\.\/\.\.\/\.\.\/coverage\/[^'"]+['"]/;
    if (coverageDirRegex.test(content)) {
        content = content.replace(coverageDirRegex, `coverageDirectory: '../../../../coverage/${newPath}'`);
        updated = true;
    }
    if (updated) {
        tree.write(jestConfigPath, content);
        devkit_1.logger.info(`  Fixed jest.config.ts paths`);
    }
}
/**
 * Fix project.json $schema path after moving to subdirectory
 * Path changes from ../../../node_modules/... to ../../../../node_modules/...
 */
function fixProjectJsonPath(tree, applicationPath, newPath) {
    const projectJsonPath = `${applicationPath}/project.json`;
    if (!tree.exists(projectJsonPath)) {
        return;
    }
    const projectJson = (0, devkit_1.readJson)(tree, projectJsonPath);
    let updated = false;
    // Fix $schema path
    if (projectJson['$schema'] === '../../../node_modules/nx/schemas/project-schema.json') {
        projectJson['$schema'] = '../../../../node_modules/nx/schemas/project-schema.json';
        updated = true;
    }
    // Update sourceRoot to use the new path
    if (projectJson.sourceRoot && !projectJson.sourceRoot.includes(newPath)) {
        projectJson.sourceRoot = `${newPath}/src`;
        updated = true;
    }
    if (updated) {
        (0, devkit_1.writeJson)(tree, projectJsonPath, projectJson);
        devkit_1.logger.info(`  Fixed project.json paths`);
    }
}
/**
 * Fix webpack.config.js output path after moving to subdirectory
 * Path changes from ../../../dist/unisphere/applications/... to ../../../../dist/...
 */
function fixWebpackConfigPath(tree, applicationPath, newPath) {
    const webpackConfigPath = `${applicationPath}/webpack.config.js`;
    if (!tree.exists(webpackConfigPath)) {
        return;
    }
    let content = tree.read(webpackConfigPath, 'utf-8');
    if (!content) {
        return;
    }
    let updated = false;
    // Fix output path - look for various patterns
    const outputPathRegex = /path:\s*join\(__dirname,\s*['"]\.\.\/\.\.\/\.\.\/dist\/unisphere\/applications\/[^'"]+['"]\)/;
    if (outputPathRegex.test(content)) {
        content = content.replace(outputPathRegex, `path: join(__dirname, '../../../../dist/${newPath}')`);
        updated = true;
    }
    if (updated) {
        tree.write(webpackConfigPath, content);
        devkit_1.logger.info(`  Fixed webpack.config.js output path`);
    }
}
/**
 * Update tsconfig.base.json paths for the application
 */
function updateTsConfigBasePaths(tree, oldPath, newPath) {
    if (!tree.exists('tsconfig.base.json')) {
        return;
    }
    const tsconfigBase = (0, devkit_1.readJson)(tree, 'tsconfig.base.json');
    if (!tsconfigBase.compilerOptions?.paths) {
        return;
    }
    let updated = false;
    for (const [key, paths] of Object.entries(tsconfigBase.compilerOptions.paths)) {
        if (Array.isArray(paths)) {
            const newPaths = paths.map((p) => {
                if (p.startsWith(oldPath + '/') || p === oldPath) {
                    updated = true;
                    return p.replace(oldPath, newPath);
                }
                return p;
            });
            tsconfigBase.compilerOptions.paths[key] = newPaths;
        }
    }
    if (updated) {
        (0, devkit_1.writeJson)(tree, 'tsconfig.base.json', tsconfigBase);
        devkit_1.logger.info(`  Updated tsconfig.base.json paths`);
    }
}
/**
 * Update root package.json scripts that reference the application
 */
function updateRootPackageJsonScripts(tree, oldPath, newPath) {
    if (!tree.exists('package.json')) {
        return;
    }
    const packageJson = (0, devkit_1.readJson)(tree, 'package.json');
    if (!packageJson.scripts) {
        return;
    }
    let updated = false;
    for (const [scriptName, scriptValue] of Object.entries(packageJson.scripts)) {
        if (typeof scriptValue === 'string' && scriptValue.includes(oldPath)) {
            packageJson.scripts[scriptName] = scriptValue.replace(new RegExp(oldPath.replace(/[/\\]/g, '[\\\\/]'), 'g'), newPath);
            updated = true;
        }
    }
    if (updated) {
        (0, devkit_1.writeJson)(tree, 'package.json', packageJson);
        devkit_1.logger.info(`  Updated root package.json scripts`);
    }
}
/**
 * Move all files from one tree path to another.
 * Recursively walks the source directory and copies each file to the destination,
 * then deletes the source. This replaces moveGenerator's moveProjectFiles.
 */
function moveFilesInTree(tree, fromPath, toPath) {
    let movedCount = 0;
    function walkAndMove(currentPath) {
        const children = tree.children(currentPath);
        for (const child of children) {
            const childPath = `${currentPath}/${child}`;
            if (tree.isFile(childPath)) {
                const content = tree.read(childPath);
                if (content) {
                    const relativePath = childPath.substring(fromPath.length + 1);
                    const newFilePath = `${toPath}/${relativePath}`;
                    tree.write(newFilePath, content);
                    movedCount++;
                }
                tree.delete(childPath);
            }
            else {
                walkAndMove(childPath);
            }
        }
    }
    walkAndMove(fromPath);
    // Delete the now-empty source directory
    if (tree.exists(fromPath)) {
        tree.delete(fromPath);
    }
    return movedCount;
}
/**
 * Update project.json at the new location after moving files.
 * Updates root, sourceRoot, $schema, and path references in target configurations.
 */
function updateProjectJsonAfterMove(tree, oldPath, newPath) {
    const projectJsonPath = `${newPath}/project.json`;
    if (!tree.exists(projectJsonPath)) {
        return;
    }
    const projectJson = (0, devkit_1.readJson)(tree, projectJsonPath);
    // Update root
    projectJson.root = newPath;
    // Update sourceRoot
    if (projectJson.sourceRoot) {
        projectJson.sourceRoot = projectJson.sourceRoot.replace(oldPath, newPath);
    }
    // Update $schema relative path
    if (projectJson['$schema']) {
        projectJson['$schema'] = projectJson['$schema'].replace(/^(\.\.\/)+/, (match) => `../${match}`);
    }
    // Update path references in targets
    if (projectJson.targets) {
        const targetStr = JSON.stringify(projectJson.targets);
        // Replace all occurrences of old path with new path in targets
        const updatedTargetStr = targetStr.replace(new RegExp(oldPath.replace(/[/\\]/g, '[\\\\/]'), 'g'), newPath);
        projectJson.targets = JSON.parse(updatedTargetStr);
    }
    (0, devkit_1.writeJson)(tree, projectJsonPath, projectJson);
}
/**
 * Ensure project.json has the correct name and will be discovered by Nx.
 * We don't use removeProjectConfiguration/updateProjectConfiguration because they can
 * cause the project.json file to be deleted. Instead, we rely on Nx's auto-discovery
 * of projects from project.json files at their new locations.
 */
function updateNxProjectRegistration(tree, oldProjectName, newProjectName, newPath) {
    // Verify project.json exists at new location
    const projectJsonPath = `${newPath}/project.json`;
    if (!tree.exists(projectJsonPath)) {
        devkit_1.logger.warn(`  Cannot update Nx registration: project.json not found at ${projectJsonPath}`);
        return;
    }
    // Read and update the project.json to ensure it has the correct name
    const projectJson = (0, devkit_1.readJson)(tree, projectJsonPath);
    // Update the name if needed
    if (projectJson.name !== newProjectName) {
        projectJson.name = newProjectName;
        (0, devkit_1.writeJson)(tree, projectJsonPath, projectJson);
        devkit_1.logger.info(`  Updated project.json name to: ${newProjectName}`);
    }
    else {
        devkit_1.logger.info(`  Project.json name already correct: ${newProjectName}`);
    }
    // Nx will auto-discover the project from its project.json file at the new location
    devkit_1.logger.info(`  Project will be auto-discovered by Nx at: ${newPath}`);
}
/**
 * Update cross-project build target references.
 * Scans all projects in the workspace for references to the old project name
 * in their target configurations and updates them.
 */
function updateCrossProjectReferences(tree, oldProjectName, newProjectName) {
    if (oldProjectName === newProjectName) {
        return; // No rename, no cross-references to update
    }
    const projects = (0, devkit_1.getProjects)(tree);
    let updatedCount = 0;
    for (const [name, config] of projects) {
        if (!config.targets)
            continue;
        let configStr = JSON.stringify(config.targets);
        // Match patterns like "old-project:build" or "old-project:build:production"
        const pattern = new RegExp(`${oldProjectName.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&')}:`, 'g');
        if (pattern.test(configStr)) {
            configStr = configStr.replace(pattern, `${newProjectName}:`);
            config.targets = JSON.parse(configStr);
            (0, devkit_1.updateProjectConfiguration)(tree, name, config);
            updatedCount++;
        }
    }
    if (updatedCount > 0) {
        devkit_1.logger.info(`  Updated build target references in ${updatedCount} project(s)`);
    }
}
/**
 * Migrate a single application using manual tree operations.
 *
 * We bypass Nx's moveGenerator because it calls readAndCombineAllProjectConfigurations
 * which uses the native WorkspaceContext (Rust addon) that caches stale file paths
 * from the packages migration. The Rust addon's glob cache cannot be reliably reset
 * from JavaScript. Manual tree operations avoid this entirely since Tree operates
 * on an in-memory overlay on top of the filesystem.
 */
async function migrateApplication(tree, target) {
    const { application: app, newPath, newNxProjectName } = target;
    devkit_1.logger.info(`Moving ${app.name} to ${target.subdirectory}/`);
    devkit_1.logger.info(`  From: ${app.currentPath}`);
    devkit_1.logger.info(`  To: ${newPath}`);
    devkit_1.logger.info(`  Nx project: ${app.currentNxProjectName}`);
    // Verify source exists before attempting move (safety check)
    const sourceProjectJson = `${app.currentPath}/project.json`;
    if (!tree.exists(sourceProjectJson)) {
        devkit_1.logger.warn(`  Source project.json not found at ${sourceProjectJson}, skipping`);
        return;
    }
    // Step 1: Clean node_modules from destination on disk if present.
    const destNodeModules = (0, path_1.join)(tree.root, newPath, 'node_modules');
    if ((0, fs_1.existsSync)(destNodeModules)) {
        devkit_1.logger.info(`  Removing leftover node_modules at destination`);
        (0, fs_1.rmSync)(destNodeModules, { recursive: true, force: true });
    }
    // Step 2: Move all files from old path to new path in the tree
    const movedCount = moveFilesInTree(tree, app.currentPath, newPath);
    devkit_1.logger.info(`  Moved ${movedCount} files from ${app.currentPath} to ${newPath}`);
    // Step 3: Update project.json at new location (root, sourceRoot, targets)
    updateProjectJsonAfterMove(tree, app.currentPath, newPath);
    devkit_1.logger.info(`  Updated project.json paths`);
    // Step 4: Update Nx project registration (remove old, register new)
    updateNxProjectRegistration(tree, app.currentNxProjectName, newNxProjectName, newPath);
    // Step 5: Update cross-project build target references
    updateCrossProjectReferences(tree, app.currentNxProjectName, newNxProjectName);
    // Step 6: Update .unisphere configuration
    updateUnisphereSourceRoot(tree, app.name, newPath);
    devkit_1.logger.info(`  Updated .unisphere sourceRoot`);
    // Step 7: Update package-lock.json
    updatePackageLockPaths(tree, app.currentPath, newPath);
    // Step 8: Fix .eslintrc.json extends path
    fixEslintConfigPath(tree, newPath);
    // Step 9: Fix tsconfig.json extends path
    fixTsconfigJsonPath(tree, newPath);
    // Step 10: Fix jest.config.ts paths
    fixJestConfigPath(tree, newPath, newPath);
    // Step 11: Fix project.json paths ($schema, sourceRoot)
    fixProjectJsonPath(tree, newPath, newPath);
    // Step 12: Fix webpack.config.js output path
    fixWebpackConfigPath(tree, newPath, newPath);
    // Step 13: Update tsconfig.base.json paths
    updateTsConfigBasePaths(tree, app.currentPath, newPath);
    // Step 14: Update root package.json scripts
    updateRootPackageJsonScripts(tree, app.currentPath, newPath);
}
/**
 * Main migration function
 */
async function update(tree) {
    devkit_1.logger.info('');
    devkit_1.logger.info('========================================');
    devkit_1.logger.info('Reorganizing applications by distribution channel');
    devkit_1.logger.info('========================================');
    devkit_1.logger.info('');
    // Phase 1: Discover applications
    const applications = discoverApplications(tree);
    if (applications.length === 0) {
        devkit_1.logger.info('No applications found to migrate');
        return;
    }
    devkit_1.logger.info(`Found ${applications.length} application(s) to reorganize`);
    // Phase 2: Calculate migration targets
    const targets = applications.map(calculateMigrationTarget);
    // Group by subdirectory for logging
    const byChannel = {
        local: targets.filter((t) => t.subdirectory === 'local'),
        server: targets.filter((t) => t.subdirectory === 'server'),
    };
    devkit_1.logger.info('');
    devkit_1.logger.info('Migration plan:');
    devkit_1.logger.info(`  - local/ (distributionChannel: none): ${byChannel['local'].length} application(s)`);
    devkit_1.logger.info(`  - server/ (distributionChannel: unisphere): ${byChannel['server'].length} application(s)`);
    devkit_1.logger.info('');
    // Phase 3: Validate preconditions
    try {
        validateMigrationPreconditions(tree, targets);
    }
    catch (error) {
        devkit_1.logger.error(`Migration precondition failed: ${error}`);
        throw error;
    }
    // Phase 4: Execute migrations SEQUENTIALLY
    let migrated = 0;
    for (const target of targets) {
        try {
            await migrateApplication(tree, target);
            migrated++;
            devkit_1.logger.info(`  [${migrated}/${targets.length}] Migrated ${target.application.name}`);
            devkit_1.logger.info('');
        }
        catch (error) {
            devkit_1.logger.error(`Failed to migrate ${target.application.name}: ${error}`);
            throw error;
        }
    }
    // Format all files at the end
    await (0, devkit_1.formatFiles)(tree);
    devkit_1.logger.info('');
    devkit_1.logger.info('========================================');
    devkit_1.logger.info(`Successfully reorganized ${migrated} application(s)`);
    devkit_1.logger.info('========================================');
    devkit_1.logger.info('');
    devkit_1.logger.info('Next steps:');
    devkit_1.logger.info('  1. Run: npm install');
    devkit_1.logger.info('  2. Run: npm run build');
    devkit_1.logger.info('  3. Verify all imports are updated');
    devkit_1.logger.info('');
}
