/**
 * Copyright (c) Siemens 2016 - 2025
 * SPDX-License-Identifier: MIT
 */
import { SchematicsException } from '@angular-devkit/schematics';
import * as ts from 'typescript';
import { discoverSourceFiles, getImportSpecifiers } from '../../utils/index.js';
import { ACTION_DIALOG_TYPES_REPLACEMENTS, ACTION_MODAL_SYMBOLS, DIALOG_METHOD_CONFIGS, LEGACY_METHODS } from './action-modal.mappings.js';
/**
 * Migration rule for action modal methods.
 * @param options - The options object containing the path to the project files to be migrated.
 * @returns A schematic rule for migrating action modal methods.
 */
export const actionModalMigrationRule = (options) => {
    return async (tree, context) => {
        context.logger.info('🔄 Migrating action modal methods to v48...');
        const tsSourceFiles = await discoverSourceFiles(tree, context, options.path);
        for (const filePath of tsSourceFiles) {
            const content = tree.read(filePath);
            if (!content) {
                continue;
            }
            const sourceFile = ts.createSourceFile(filePath, content.toString(), ts.ScriptTarget.Latest, true);
            // Match both @siemens/element-ng and @simpl/element-ng, with or without /action-modal
            // This ensures we only process files that import from these modules
            const modulePathToMatch = /@(siemens|simpl)\/element-ng(\/action-modal)?/;
            const actionModalImports = getImportSpecifiers(sourceFile, modulePathToMatch, ACTION_MODAL_SYMBOLS);
            if (!actionModalImports?.length) {
                continue;
            }
            const pendingTransformations = [];
            const usedIdentifier = new Set();
            const visitNodeAndCollectTransformations = (node) => {
                if (ts.isImportDeclaration(node)) {
                    return;
                }
                // Collect method call transformations
                if (ts.isCallExpression(node)) {
                    const methodTransformation = createActionDialogMethodCallTransformation(node);
                    if (methodTransformation) {
                        pendingTransformations.push(methodTransformation);
                    }
                }
                // Collect type reference transformations
                if (ts.isPropertyAccessExpression(node)) {
                    const typeTransformation = createActionDialogTypeTransformation(node);
                    if (typeTransformation) {
                        pendingTransformations.push(typeTransformation);
                        // We remove the usage here. In order to avoid counting it as used identifier, we skip further processing.
                        return;
                    }
                }
                if (ts.isIdentifier(node)) {
                    usedIdentifier.add(node.text);
                }
                node.forEachChild(visitNodeAndCollectTransformations);
            };
            sourceFile.forEachChild(visitNodeAndCollectTransformations);
            pendingTransformations.push(...removeUnusedImports(sourceFile, usedIdentifier));
            if (pendingTransformations.length > 0) {
                applyCodeTransformations(tree, filePath, pendingTransformations);
            }
        }
        return tree;
    };
};
const createActionDialogMethodCallTransformation = (node) => {
    const expression = node.expression;
    if (!ts.isPropertyAccessExpression(expression)) {
        return null;
    }
    const methodName = expression.name.text;
    if (!LEGACY_METHODS.includes(methodName)) {
        return null;
    }
    const dialogPropertiesArray = generateDialogProperties(methodName, node.arguments);
    const printer = ts.createPrinter();
    const newNode = ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(expression.expression, ts.factory.createIdentifier('showActionDialog')), undefined, [ts.factory.createObjectLiteralExpression(dialogPropertiesArray)]);
    const newCode = printer.printNode(ts.EmitHint.Expression, newNode, node.getSourceFile());
    return {
        node,
        newCode
    };
};
const createActionDialogTypeTransformation = (node) => {
    const nodeText = node.getText().trim();
    const matchingReplacement = ACTION_DIALOG_TYPES_REPLACEMENTS.find(typeReplacement => nodeText === typeReplacement.old);
    if (!matchingReplacement) {
        return null;
    }
    return {
        node,
        newCode: `'${matchingReplacement.new}'`
    };
};
/**
 * Generates the dialog properties for the action dialog.
 * @param methodName - The name of the method being migrated.
 * @param nodeArguments - The arguments passed to the method.
 * @returns An array of PropertyAssignment nodes for TypeScript printer.
 * @throws SchematicsException if the method configuration is unknown.
 */
const generateDialogProperties = (methodName, nodeArguments) => {
    const methodConfig = DIALOG_METHOD_CONFIGS[methodName];
    if (!methodConfig) {
        throw new SchematicsException(`Unknown method configuration for: ${methodName}`);
    }
    const dialogProperties = [
        ts.factory.createPropertyAssignment(ts.factory.createIdentifier('type'), ts.factory.createStringLiteral(methodConfig.type, true))
    ];
    nodeArguments.forEach((arg, index) => {
        const paramName = methodConfig.parameters[index];
        if (paramName) {
            dialogProperties.push(ts.factory.createPropertyAssignment(ts.factory.createIdentifier(paramName), arg));
        }
    });
    return dialogProperties;
};
const applyCodeTransformations = (tree, filePath, codeTransformations) => {
    const recorder = tree.beginUpdate(filePath);
    // Sort by position (descending) to avoid offset issues
    codeTransformations
        .sort((a, b) => b.node.getStart() - a.node.getStart())
        .forEach(({ node, newCode }) => {
        recorder.remove(node.getStart(), node.getWidth());
        recorder.insertLeft(node.getStart(), newCode);
    });
    tree.commitUpdate(recorder);
};
const removeUnusedImports = (sourceFile, usedIdentifier) => {
    const printer = ts.createPrinter();
    const importFinder = (node) => {
        if (ts.isImportDeclaration(node) &&
            ts.isStringLiteral(node.moduleSpecifier) &&
            /@(siemens|simpl)\/element-ng(\/action-modal)?/.test(node.moduleSpecifier.text) &&
            node.importClause?.namedBindings &&
            ts.isNamedImports(node.importClause.namedBindings)) {
            const usedBindings = node.importClause.namedBindings.elements.filter(element => 
            // This script anyway is not capable of handling aliasing imports, so we can ignore them here.
            usedIdentifier.has(element.name.text) || !ACTION_MODAL_SYMBOLS.includes(element.name.text));
            if (usedBindings.length === node.importClause.namedBindings.elements.length) {
                // All bindings are used, no changes needed
                return undefined;
            }
            if (usedBindings.length === 0) {
                // No bindings are used, remove the entire import statement
                return {
                    node,
                    newCode: ''
                };
            }
            // Recreate import statement with only used bindings
            const newImport = ts.factory.createImportDeclaration(node.modifiers, ts.factory.createImportClause(node.importClause.isTypeOnly, node.importClause.name, ts.factory.createNamedImports(usedBindings)), node.moduleSpecifier, node.attributes);
            return {
                node,
                newCode: printer.printNode(ts.EmitHint.Unspecified, newImport, sourceFile)
            };
        }
        return undefined;
    };
    return sourceFile.statements.map(importFinder).filter(transform => transform !== undefined);
};
