"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.discoverAvailableFlavors = discoverAvailableFlavors;
exports.resolveTargetFile = resolveTargetFile;
exports.addVisualGenerator = addVisualGenerator;
const tslib_1 = require("tslib");
const devkit_1 = require("@nx/devkit");
const path = tslib_1.__importStar(require("path"));
const ts_morph_1 = require("ts-morph");
const utils_1 = require("../utils");
const visual_utils_1 = require("./visual-utils");
function discoverAvailableFlavors(tree, sourceRoot) {
    const libDir = `${sourceRoot}/src/lib`;
    if (!tree.exists(libDir)) {
        return [];
    }
    return tree
        .children(libDir)
        .filter((f) => /^(runtime-.+|.+-runtime-base)\.tsx$/.test(f))
        .map((f) => f.replace('.tsx', ''))
        .sort();
}
function resolveTargetFile(tree, sourceRoot, flavor) {
    const libDir = `${sourceRoot}/src/lib`;
    if (!flavor) {
        const defaultPath = `${libDir}/runtime.tsx`;
        if (!tree.exists(defaultPath)) {
            throw new Error(`Default runtime file not found at ${defaultPath}`);
        }
        return defaultPath;
    }
    const targetPath = `${libDir}/${flavor}.tsx`;
    if (!tree.exists(targetPath)) {
        const available = discoverAvailableFlavors(tree, sourceRoot);
        throw new Error(`Flavor file '${flavor}.tsx' not found in ${libDir}.\n` +
            (available.length > 0
                ? `  Available flavors: ${available.join(', ')}\n`
                : '') +
            `  Omit --flavor to target the default runtime.`);
    }
    return targetPath;
}
function validateOptions(options) {
    if (!options.runtimeName || options.runtimeName.trim() === '') {
        throw new Error(`Missing required option: 'runtimeName'\n` +
            `  Description: The name of the runtime to add the visual to\n` +
            `  Type: string\n` +
            `  Pattern: ^[a-zA-Z][a-zA-Z0-9\\-\\s]*$\n` +
            `  Example: nx g @unisphere/nx:add-visual --runtimeName=my-runtime --visualName=my-visual`);
    }
    const namePattern = /^[a-zA-Z][a-zA-Z0-9\-\s]*$/;
    if (!namePattern.test(options.runtimeName)) {
        throw new Error(`Invalid value '${options.runtimeName}' for option 'runtimeName'\n` +
            `  Pattern: ^[a-zA-Z][a-zA-Z0-9\\-\\s]*$\n` +
            `  Received: ${options.runtimeName}`);
    }
    if (!options.visualName || options.visualName.trim() === '') {
        throw new Error(`Missing required option: 'visualName'\n` +
            `  Description: The name of the visual to create\n` +
            `  Type: string\n` +
            `  Pattern: ^[a-zA-Z][a-zA-Z0-9\\-\\s]*$\n` +
            `  Example: nx g @unisphere/nx:add-visual --runtimeName=my-runtime --visualName=my-visual`);
    }
    if (!namePattern.test(options.visualName)) {
        throw new Error(`Invalid value '${options.visualName}' for option 'visualName'\n` +
            `  Pattern: ^[a-zA-Z][a-zA-Z0-9\\-\\s]*$\n` +
            `  Received: ${options.visualName}`);
    }
    if (options.isSingleOccurrence !== undefined && typeof options.isSingleOccurrence !== 'boolean') {
        throw new Error(`Invalid value for option 'isSingleOccurrence'\n` +
            `  Type: boolean\n` +
            `  Received: ${options.isSingleOccurrence}`);
    }
}
/**
 * Adds necessary imports (runtimeVisualSettings, runtimeVisualSchema) to the runtime file.
 */
function addImportToRuntime(sourceFilePath, tree, vars, project) {
    const sourceFile = (0, visual_utils_1.loadSourceFile)(tree, sourceFilePath, project);
    const { visualName__humanReadable: humanName, runtimeVisualSettings, runtimeVisualSchema, typesAlias, } = vars;
    // Check if import already exists to prevent duplicates
    const alreadyExists = sourceFile
        .getImportDeclarations()
        .some((decl) => decl
        .getNamedImports()
        .some((imp) => imp.getName() === runtimeVisualSettings));
    if (alreadyExists) {
        console.log(`⚠️ Import for visual ${humanName} already exists.`);
        return;
    }
    sourceFile.addImportDeclaration({
        kind: ts_morph_1.StructureKind.ImportDeclaration,
        namedImports: [runtimeVisualSettings, runtimeVisualSchema],
        moduleSpecifier: typesAlias,
    }); // Explicit cast for clarity
    (0, visual_utils_1.saveSourceFile)(tree, sourceFilePath, sourceFile.getFullText());
    console.log(`✅ Added import for visual ${humanName}`);
}
function updateRuntimeIndexExport(tree, runtimeName, visualName, typesPackageInfo, visualsSubfolder) {
    const runtimeIndexPath = `${typesPackageInfo.basePath}/src/lib/${runtimeName}-runtime/index.ts`;
    if (!tree.exists(runtimeIndexPath)) {
        throw new Error(`Runtime index file not found at ${runtimeIndexPath}`);
    }
    const currentContent = tree.read(runtimeIndexPath, 'utf-8');
    if (!currentContent) {
        throw new Error(`Failed to read content from ${runtimeIndexPath}`);
    }
    const exportLine = `export * from './${visualsSubfolder}/${visualName}-visual-types';`;
    // Check if the export already exists
    if (currentContent.includes(exportLine)) {
        return; // Already exported
    }
    // Add the export at the end of the file
    const updatedContent = currentContent.replace(/\n\n$/, '\n') + exportLine;
    tree.write(runtimeIndexPath, updatedContent);
}
/**
 * Adds a new case to the primary switch statement in the runtime file.
 */
function addSwitchCaseToRuntime(sourceFilePath, tree, vars, project) {
    const sourceFile = (0, visual_utils_1.loadSourceFile)(tree, sourceFilePath, project);
    const { visualName__camelCase: visualCamel, visualName__pascalCase: visualPascal, visualName__humanReadable: humanName, } = vars;
    const switchStatement = sourceFile.getDescendantsOfKind(ts_morph_1.SyntaxKind.SwitchStatement)[0];
    (0, visual_utils_1.throwIfNotFound)(switchStatement, `No switch statement found in ${sourceFilePath}`);
    const caseBlock = switchStatement.getCaseBlock();
    const cases = caseBlock.getClauses();
    const exists = cases.some((c) => {
        if (c.getKind() === ts_morph_1.SyntaxKind.CaseClause) {
            const clause = c;
            return (clause.getExpression()?.getText().replace(/['"]/g, '') === visualCamel);
        }
        return false;
    });
    if (exists) {
        console.log(`⚠️ Switch case for visual ${humanName} already exists.`);
        return;
    }
    const newCase = `case '${visualCamel}':
          this._render${visualPascal}(visual);
          return true`;
    const defaultIndex = cases.findIndex((c) => c.getKind() === ts_morph_1.SyntaxKind.DefaultClause);
    if (defaultIndex !== -1) {
        const defaultClause = cases[defaultIndex];
        defaultClause.replaceWithText(`${newCase}\n${defaultClause.getText()}`);
    }
    else {
        const lastCase = cases[cases.length - 1];
        lastCase.replaceWithText(`${lastCase.getText()}\n${newCase}`);
    }
    (0, visual_utils_1.saveSourceFile)(tree, sourceFilePath, sourceFile.getFullText());
    console.log(`✅ Added switch case for visual ${humanName}`);
}
/**
 * Adds a new visual type entry to the `visualTypes` object within the constructor's super call.
 */
function addVisualTypesToConstructor(sourceFilePath, tree, vars, project, isSingleOccurrence) {
    const sourceFile = (0, visual_utils_1.loadSourceFile)(tree, sourceFilePath, project);
    const { visualName__camelCase: visualCamel, visualName__humanReadable: humanName, runtimeVisualSchema, } = vars;
    const clazz = (0, visual_utils_1.findFirstClass)(sourceFile);
    const constructor = (0, visual_utils_1.findFirstConstructor)(clazz);
    const superCall = (0, visual_utils_1.findSuperCall)(constructor);
    const objectLiteral = (0, visual_utils_1.getObjectLiteralFromFirstArg)(superCall);
    const visualTypesProp = (0, visual_utils_1.throwIfNotFound)(objectLiteral
        .getProperties()
        .find((prop) => prop.getKind() === ts_morph_1.SyntaxKind.PropertyAssignment &&
        ['visualTypes', 'flavorVisualTypes'].includes(prop.getName())), `Could not find visualTypes or flavorVisualTypes property in super() call in ${sourceFilePath}`);
    const visualTypesObject = (0, visual_utils_1.throwIfNotFound)(visualTypesProp.getInitializer(), // Assert type after getting initializer
    `visualTypes is not an object literal in ${sourceFilePath}`);
    if ((0, visual_utils_1.propertyExists)(visualTypesObject, visualCamel)) {
        console.log(`⚠️ Visual type for visual ${humanName} already exists.`);
        return;
    }
    visualTypesObject.addPropertyAssignment({
        name: `'${visualCamel}'`, // Ensure the key is consistently quoted for string literals
        initializer: `{ singleton: ${isSingleOccurrence}, schema: ${runtimeVisualSchema} }`,
    });
    (0, visual_utils_1.saveSourceFile)(tree, sourceFilePath, sourceFile.getFullText());
    console.log(`✅ Added visual type for ${humanName} to visualTypes.`);
}
/**
 * Adds a private render method to the main class in the runtime file.
 * Uses Nx's generateFiles for proper template processing.
 */
function addRenderMethod(sourceFilePath, tree, vars, project, typesPackageInfo, visualsSubfolder) {
    const sourceFile = (0, visual_utils_1.loadSourceFile)(tree, sourceFilePath, project);
    const methodName = `_render${vars.visualName__pascalCase}`;
    const clazz = (0, visual_utils_1.findFirstClass)(sourceFile);
    // Always check and add required imports first (even if method exists)
    let importsModified = false;
    // Check if ThemeProvider import exists, add if missing
    const themeProviderImportExists = sourceFile
        .getImportDeclarations()
        .some((decl) => decl.getModuleSpecifier().getLiteralValue() ===
        '@kaltura/ds-react-theme' &&
        decl.getNamedImports().some((imp) => imp.getName() === 'ThemeProvider'));
    if (!themeProviderImportExists) {
        sourceFile.addImportDeclaration({
            kind: ts_morph_1.StructureKind.ImportDeclaration,
            namedImports: ['ThemeProvider'],
            moduleSpecifier: '@kaltura/ds-react-theme',
        });
        console.log(`✅ Added ThemeProvider import to ${sourceFilePath}`);
        importsModified = true;
    }
    // Check if ScopedUnisphereWorkspaceProvider import exists, add if missing
    const scopedProviderImportExists = sourceFile
        .getImportDeclarations()
        .some((decl) => decl.getModuleSpecifier().getLiteralValue() ===
        '@unisphere/runtime-react' &&
        decl
            .getNamedImports()
            .some((imp) => imp.getName() === 'ScopedUnisphereWorkspaceProvider'));
    if (!scopedProviderImportExists) {
        // Check if there's already an import from @unisphere/runtime-react
        const existingRuntimeReactImport = sourceFile
            .getImportDeclarations()
            .find((decl) => decl.getModuleSpecifier().getLiteralValue() ===
            '@unisphere/runtime-react');
        if (existingRuntimeReactImport) {
            // Add to existing import
            existingRuntimeReactImport.addNamedImport('ScopedUnisphereWorkspaceProvider');
        }
        else {
            // Create new import
            sourceFile.addImportDeclaration({
                kind: ts_morph_1.StructureKind.ImportDeclaration,
                namedImports: ['ScopedUnisphereWorkspaceProvider'],
                moduleSpecifier: '@unisphere/runtime-react',
            });
        }
        console.log(`✅ Added ScopedUnisphereWorkspaceProvider import to ${sourceFilePath}`);
        importsModified = true;
    }
    // Check if method already exists.
    const exists = clazz
        .getInstanceMethods()
        .some((m) => m.getName() === methodName);
    if (exists) {
        // Save imports if they were modified, even though method exists
        if (importsModified) {
            (0, visual_utils_1.saveSourceFile)(tree, sourceFilePath, sourceFile.getFullText());
        }
        console.log(`⚠️ Method ${methodName} already exists.`);
        return;
    }
    // Use Nx's generateFiles to process the template to a temporary location
    const tempDir = 'temp-render-method';
    (0, devkit_1.generateFiles)(tree, path.join(__dirname, 'templates', 'new-visual'), tempDir, {
        ...vars,
        tmpl: '', // This removes the .template extension
    });
    updateRuntimeIndexExport(tree, vars.runtimeName__lowerDashCase, vars.visualName__lowerDashCase, typesPackageInfo, visualsSubfolder);
    // Read the processed template content
    const processedContent = (0, visual_utils_1.readTreeFile)(tree, `${tempDir}/render-method`);
    // Clean up the temporary file
    tree.delete(`${tempDir}/render-method`);
    // Add the private method to the class using ts-morph's addMethod
    clazz.addMethod({
        name: methodName,
        parameters: [{ name: 'visual', type: 'HtmlDomRuntimeVisual<Root>' }],
        statements: processedContent,
        scope: ts_morph_1.Scope.Private,
    });
    (0, visual_utils_1.saveSourceFile)(tree, sourceFilePath, sourceFile.getFullText());
    console.log(`✅ Added render method ${methodName}.`);
}
/**
 * Adds an interface and schema definition to the types/core runtime types file.
 */
function createVisualTypes(tree, vars, typesPackageInfo, visualsSubfolder) {
    // Check if the runtime types file already exists
    const visualTypesPath = `${typesPackageInfo.basePath}/src/lib/${vars.runtimeName__lowerDashCase}-runtime/${visualsSubfolder}/${vars.visualName__lowerDashCase}-visual-types.ts`;
    const packageName = typesPackageInfo.hasTypesPackage ? 'types' : 'core';
    if (tree.exists(visualTypesPath)) {
        throw new Error(`Visual types file "${vars.visualName__lowerDashCase}-visual-types.ts" already exists in ${packageName} package.\n` +
            'Please choose a different visual name or remove the existing file first.');
    }
    const { runtimeVisualSettings: interfaceName, runtimeVisualSchema: schemaName, visualName__humanReadable: humanName, } = vars;
    // Construct the new interface and schema content.
    const newContent = `import { ValidatorSchema } from '@unisphere/core';

export interface ${interfaceName} {
  schemaVersion?: string;
}

export const ${schemaName}: ValidatorSchema = {
  type: 'object',
  properties: {
    schemaVersion: { type: 'literal', value: '1', optional: true },
  },
};
`;
    // Append the new content to the file.
    tree.write(visualTypesPath, newContent);
    console.log(`✅ Added interface and schema for ${humanName}`);
}
/* -------------------------------------------
 * Entry Point (Main generator function)
 * -----------------------------------------*/
/**
 * Nx generator to create a new visual within a specified runtime.
 */
async function addVisualGenerator(tree, options) {
    validateOptions(options);
    // Validate and read .unisphere configuration first
    const unisphereConfig = (0, utils_1.validateUnisphereConfig)(tree);
    // After validation, these are guaranteed to be strings
    const runtimeName = options.runtimeName;
    const visualName = options.visualName;
    // Validate that the runtime exists
    if (!unisphereConfig.runtimes[runtimeName]) {
        const availableRuntimes = Object.keys(unisphereConfig.runtimes);
        throw new Error(`Runtime '${runtimeName}' not found in .unisphere configuration.\n` +
            (availableRuntimes.length > 0
                ? `  Available runtimes: ${availableRuntimes.join(', ')}\n`
                : `  No runtimes found. Please create a runtime first using: nx generate @unisphere/nx:add-runtime\n`) +
            `  Example: nx g @unisphere/nx:add-visual --runtimeName=${availableRuntimes[0] || 'my-runtime'} --visualName=my-visual`);
    }
    const isSingleOccurrence = options.isSingleOccurrence ?? false;
    // Extract environment variables
    const widgetName = unisphereConfig.name;
    const runtimeSourceRoot = (0, utils_1.validateRuntimeExists)(tree, runtimeName);
    const targetFile = resolveTargetFile(tree, runtimeSourceRoot, options.flavor);
    const typesPackageInfo = (0, utils_1.findTypesOrCorePackageInfo)(tree);
    const runtimeNames = (0, utils_1.createNameTransforms)(runtimeName, 'runtimeName');
    const visualNames = (0, utils_1.createNameTransforms)(visualName, 'visualName');
    const widgetNames = (0, utils_1.createNameTransforms)(widgetName, 'widgetName');
    const runtimeVisualSettings = `${runtimeNames.runtimeName__pascalCase}Runtime${visualNames.visualName__pascalCase}VisualSettings`;
    const runtimeVisualSchema = `${runtimeNames.runtimeName__camelCase}Runtime${visualNames.visualName__pascalCase}VisualSettingsSchema`;
    const templateVariables = {
        ...runtimeNames,
        ...visualNames,
        ...widgetNames,
        runtimeVisualSettings,
        runtimeVisualSchema,
        typesAlias: typesPackageInfo.alias,
    };
    const project = new ts_morph_1.Project({
        useInMemoryFileSystem: true,
        skipAddingFilesFromTsConfig: true,
    });
    if (options.flavor) {
        console.log(`\nProcessing flavor '${options.flavor}'...`);
    }
    const targetBaseName = path.basename(targetFile, '.tsx');
    const visualsSubfolder = `${targetBaseName}-visuals`;
    addImportToRuntime(targetFile, tree, templateVariables, project);
    addVisualTypesToConstructor(targetFile, tree, templateVariables, project, isSingleOccurrence);
    addSwitchCaseToRuntime(targetFile, tree, templateVariables, project);
    addRenderMethod(targetFile, tree, templateVariables, project, typesPackageInfo, visualsSubfolder);
    createVisualTypes(tree, templateVariables, typesPackageInfo, visualsSubfolder);
    await (0, devkit_1.formatFiles)(tree);
    const flavorSummary = options.flavor ? ` (flavor: ${options.flavor})` : '';
    console.log(`\n🎉 Successfully added visual '${visualName}' to runtime '${runtimeName}'${flavorSummary}!`);
    console.log(`   Runtime: ${runtimeName} (${runtimeSourceRoot})`);
    console.log(`   Target: ${targetFile}`);
    console.log(`   Visual: ${visualName}`);
    console.log(`   Single occurrence: ${isSingleOccurrence ? 'Yes' : 'No'}`);
    return () => {
        return { visualName };
    };
}
exports.default = addVisualGenerator;
