/* jscpd:ignore-start */
import { SfCommand, Flags } from '@salesforce/sf-plugins-core';
import { Messages } from '@salesforce/core';
import c from 'chalk';
import fs from 'fs-extra';
import * as path from 'path';
import { glob } from 'glob';
import { createTempDir, execCommand, isCI, removeObjectPropertyLists, uxLog } from '../../../../common/utils/index.js';
import { prompts } from '../../../../common/utils/prompts.js';
import { parsePackageXmlFile, parseXmlFile, writePackageXmlFile, writeXmlFile, } from '../../../../common/utils/xmlUtils.js';
import { getConfig, setConfig } from '../../../../config/index.js';
import { PACKAGE_ROOT_DIR } from '../../../../settings.js';
import { FilterXmlContent } from './filter-xml-content.js';
import { GLOB_IGNORE_PATTERNS } from '../../../../common/utils/projectUtils.js';
Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
const messages = Messages.loadMessages('sfdx-hardis', 'org');
export default class CleanReferences extends SfCommand {
    static title = 'Clean references in dx sources';
    static description = `
## Command Behavior

**Removes unwanted references and cleans up metadata within your Salesforce DX project sources.**

This command provides a powerful way to maintain a clean and efficient Salesforce codebase by eliminating unnecessary or problematic metadata. It supports various cleaning types, from removing hardcoded user references in dashboards to minimizing profile attributes.

Key functionalities include:

- **Configurable Cleaning Types:** You can specify a particular cleaning type (e.g., 
- **JSON/XML Configuration:** Cleaning operations can be driven by a JSON configuration file or a 
- **Interactive Selection:** If no cleaning type is specified, the command interactively prompts you to select which references to clean.
- **Persistent Configuration:** You can choose to save your cleaning selections in your project's configuration (\`.sfdx-hardis.yml\`) so they are automatically applied during future Work Save operations.
- **File Deletion:** Beyond just cleaning XML content, it can also delete related files (e.g., custom field files and their translations when a custom field is marked for deletion).

<details markdown="1">
<summary>Technical explanations</summary>

The command's technical implementation involves several steps:

- **Configuration Loading:** It reads the project's configuration to determine default cleaning types and user preferences.
- **Cleaning Type Processing:** For each selected cleaning type, it either executes a dedicated sub-command (e.g., 
- **XML Filtering:** For template-based cleanings, it constructs a temporary JSON configuration file based on predefined templates or user-provided 
- **Package.xml Cleanup:** It iterates through 
- **Object Property Removal:** The 
</details>
`;
    static examples = [
        '$ sf hardis:project:clean:references',
        '$ sf hardis:project:clean:references --type all',
        '$ sf hardis:project:clean:references --config ./cleaning/myconfig.json',
        '$ sf hardis:project:clean:references --config ./somefolder/myDestructivePackage.xml',
    ];
    // public static args = [{name: 'file'}];
    static flags = {
        type: Flags.string({
            char: 't',
            description: 'Cleaning type',
            options: [
                'all',
                'caseentitlement',
                'dashboards',
                'datadotcom',
                'destructivechanges',
                'localfields',
                'productrequest',
                'entitlement',
                'flowPositions',
                'sensitiveMetadatas',
                'minimizeProfiles'
            ],
        }),
        config: Flags.string({
            char: 'c',
            description: 'Path to a JSON config file or a destructiveChanges.xml file',
        }),
        debug: Flags.boolean({
            char: 'd',
            default: false,
            description: messages.getMessage('debugMode'),
        }),
        websocket: Flags.string({
            description: messages.getMessage('websocket'),
        }),
        skipauth: Flags.boolean({
            description: 'Skip authentication check when a default username is required',
        }),
    };
    // Set this to true if your command requires a project workspace; 'requiresProject' is false by default
    static requiresProject = true;
    /* jscpd:ignore-end */
    debugMode = false;
    cleaningTypes = [];
    allCleaningTypes = [
        {
            value: 'checkPermissions',
            title: 'Check custom items are existing it at least one Permission Set',
            command: 'sf hardis:lint:access',
        },
        {
            value: 'dashboards',
            title: 'Dashboards: Remove reference to hardcoded users',
        },
        {
            value: 'destructivechanges',
            title: 'DestructiveChanges.xml: Remove source files mentioned in destructiveChanges.xml',
        },
        {
            value: 'flowPositions',
            title: `Flows: Replace all positions in AutoLayout Flows by 0 to simplify conflicts management`,
            command: 'sf hardis:project:clean:flowpositions',
        },
        {
            value: 'sensitiveMetadatas',
            title: `Remove sensitive metadata content from sources (ex: Certificates)`,
            command: 'sf hardis:project:clean:sensitive-metadatas',
        },
        {
            value: 'listViewsMine',
            title: `ListViews: Convert scope "Everything" into scope "Mine" on ListViews`,
            command: 'sf hardis:project:clean:listviews',
        },
        {
            value: 'minimizeProfiles',
            title: 'Profiles: Remove profile attributes that exists on permission sets',
            command: 'sf hardis:project:clean:minimizeprofiles',
        },
        {
            value: 'caseentitlement',
            title: 'References to Entitlement Management items',
        },
        {
            value: 'datadotcom',
            title: 'References to Data.com items. https://help.salesforce.com/articleView?id=000320795&type=1&mode=1',
        },
        {
            value: 'entitlement',
            title: 'References to Entitlement object',
        },
        {
            value: 'localfields',
            title: 'References to Local Fields items. https://help.salesforce.com/articleView?id=sf.admin_local_name_fields.htm&type=5',
        },
        {
            value: 'productrequest',
            title: 'References to ProductRequest object',
        },
        {
            value: 'systemDebug',
            title: 'Remove System.debug from sources',
            command: 'sf hardis:project:clean:systemdebug',
        },
        {
            value: 'v60',
            title: 'Make metadata compliant with v60',
        },
    ];
    configFile;
    deleteItems = {};
    async run() {
        const { flags } = await this.parse(CleanReferences);
        this.debugMode = flags.debug || false;
        this.cleaningTypes = flags.type ? [flags.type] : [];
        this.configFile = flags.config || null;
        const config = await getConfig('project');
        // Config file sent by user
        if (this.configFile != null) {
            this.cleaningTypes = [this.configFile.trim()];
        }
        else {
            // Read list of cleanings to perform in references
            if (this.cleaningTypes.length > 0 && this.cleaningTypes[0] === 'all') {
                this.cleaningTypes = config.autoCleanTypes || [];
            }
            // Prompt user cleanings to perform
            if (!isCI && this.cleaningTypes.length === 0) {
                const typesResponse = await prompts({
                    type: 'multiselect',
                    name: 'value',
                    message: c.cyanBright('What references do you want to clean from your SFDX project sources ?'),
                    description: 'Select which types of reference cleaning to perform on your project',
                    choices: this.allCleaningTypes,
                });
                this.cleaningTypes = typesResponse.value;
            }
        }
        // Prompt user to save choice in configuration
        const autoCleanTypes = config.autoCleanTypes || [];
        const toAdd = this.cleaningTypes.filter((type) => !autoCleanTypes.includes(type));
        if (toAdd.length > 0 && !isCI && flags.type !== 'all') {
            const saveResponse = await prompts({
                type: 'confirm',
                name: 'value',
                default: true,
                message: c.cyanBright('Do you want to save this action in your project configuration, so it is executed at each Work Save ?'),
                description: 'Choose whether to automatically apply these cleaning types during future work saves',
            });
            if (saveResponse.value === true) {
                autoCleanTypes.push(...this.cleaningTypes);
                await setConfig('project', {
                    autoCleanTypes: [...new Set(autoCleanTypes)],
                });
            }
        }
        // Process cleaning
        for (const cleaningType of this.cleaningTypes) {
            const cleaningTypeObj = this.allCleaningTypes.filter((cleaningTypeObj) => cleaningTypeObj.value === cleaningType)[0];
            if (cleaningTypeObj?.command) {
                let command = cleaningTypeObj?.command;
                if (this.argv.indexOf('--websocket') > -1) {
                    command += ` --websocket ${this.argv[this.argv.indexOf('--websocket') + 1]}`;
                }
                uxLog("action", this, c.cyan(`Run cleaning command ${c.bold(cleaningType)} (${cleaningTypeObj.title}) ...`));
                // Command based cleaning
                await execCommand(command, this, {
                    fail: true,
                    output: true,
                    debug: this.debugMode,
                });
            }
            else {
                // Template based cleaning
                uxLog("action", this, c.cyan(`Apply cleaning of references to ${c.bold(cleaningType)} (${cleaningTypeObj.title})...`));
                const filterConfigFile = await this.getFilterConfigFile(cleaningType);
                const packageDirectories = this.project?.getPackageDirectories() || [];
                for (const packageDirectory of packageDirectories) {
                    await FilterXmlContent.run(['-c', filterConfigFile, '--inputfolder', packageDirectory.path, '--outputfolder', packageDirectory.path], this.config);
                }
            }
        }
        // Clean package.xml file from deleted items
        uxLog("log", this, c.grey(`Cleaning package.xml & files from deleted items...`));
        const patternPackageXml = '**/manifest/**/package*.xml';
        const packageXmlFiles = await glob(patternPackageXml, {
            cwd: process.cwd(),
            ignore: GLOB_IGNORE_PATTERNS
        });
        for (const packageXmlFile of packageXmlFiles) {
            const packageXmlContent = await parsePackageXmlFile(packageXmlFile);
            const packageXmlContentStr = JSON.stringify(packageXmlContent);
            const newPackageXmlContent = removeObjectPropertyLists(packageXmlContent, this.deleteItems);
            if (packageXmlContentStr !== JSON.stringify(newPackageXmlContent)) {
                await writePackageXmlFile(packageXmlFile, newPackageXmlContent);
                uxLog("log", this, c.grey('-- cleaned elements from ' + packageXmlFile));
            }
        }
        // Delete files when necessary (in parallel)
        uxLog("log", this, c.grey(`Removing obsolete files...`));
        await Promise.all(Object.keys(this.deleteItems).map(async (type) => {
            await this.manageDeleteRelatedFiles(type);
        }));
        uxLog("success", this, c.green(`Cleaning complete`));
        // Return an object to be displayed with --json
        return { outputString: 'Cleaned references from sfdx project' };
    }
    async getFilterConfigFile(cleaningType) {
        const templateFile = path.join(path.join(PACKAGE_ROOT_DIR, 'defaults/clean', 'template.txt'));
        // Read and complete cleaning template
        let templateContent = await fs.readFile(templateFile, 'utf8');
        if (cleaningType === 'destructivechanges' || cleaningType.endsWith('.xml')) {
            // destructive changes file
            const destructiveChangesFile = cleaningType.endsWith('.xml') ? cleaningType : './manifest/destructiveChanges.xml';
            const destructiveChanges = await parseXmlFile(destructiveChangesFile);
            for (const type of destructiveChanges.Package.types || []) {
                const members = type.members;
                templateContent = templateContent.replace(new RegExp(`{{ ${type.name[0]} }}`, 'g'), JSON.stringify(members, null, 2));
                this.deleteItems[type.name[0]] = (this.deleteItems[type.name[0]] || []).concat(members);
            }
        }
        else {
            // Predefined destructive items file
            const filterConfigFileConfigPath = cleaningType.endsWith('.json')
                ? cleaningType
                : path.join(path.join(PACKAGE_ROOT_DIR, 'defaults/clean', cleaningType + '.json'));
            const filterConfigFileConfig = JSON.parse(await fs.readFile(filterConfigFileConfigPath, 'utf8'));
            for (const type of Object.keys(filterConfigFileConfig.items)) {
                templateContent = templateContent.replace(new RegExp(`{{ ${type} }}`, 'g'), JSON.stringify(filterConfigFileConfig.items[type], null, 2));
                this.deleteItems[type] = (this.deleteItems[type] || []).concat(filterConfigFileConfig.items[type]);
            }
        }
        // Create temporary file
        templateContent = templateContent.replace(/{{ .* }}/gm, '[]');
        const tmpCleanFileName = cleaningType.endsWith('.xml') || cleaningType.endsWith('.json') ? path.basename(cleaningType) : cleaningType;
        const filterConfigFile = path.join(await createTempDir(), `clean_${tmpCleanFileName}.json`);
        await fs.writeFile(filterConfigFile, templateContent);
        return filterConfigFile;
    }
    async manageDeleteRelatedFiles(type) {
        // Custom fields
        if (type === 'CustomField') {
            for (const field of this.deleteItems[type] || []) {
                await this.manageDeleteCustomFieldRelatedFiles(field);
            }
        }
    }
    async manageDeleteCustomFieldRelatedFiles(field) {
        // Remove custom field and customTranslation
        const [obj, fld] = field.split('.');
        const patternField = `**/objects/${obj}/fields/${fld}.field-meta.xml`;
        const patternTranslation = `**/objectTranslations/${obj}-*/${fld}.fieldTranslation-meta.xml`;
        for (const pattern of [patternField, patternTranslation]) {
            const matchFiles = await glob(pattern, { cwd: process.cwd(), ignore: GLOB_IGNORE_PATTERNS });
            for (const removeFile of matchFiles) {
                await fs.remove(removeFile);
                uxLog("log", this, c.grey(`Removed file ${removeFile}`));
            }
        }
        // Remove field in recordTypes
        const patternRecordType = `**/objects/${obj}/recordTypes/*.recordType-meta.xml`;
        const matchFilesPattern = await glob(patternRecordType, {
            cwd: process.cwd(),
            ignore: GLOB_IGNORE_PATTERNS
        });
        for (const recordTypeFile of matchFilesPattern) {
            const recordType = await parseXmlFile(recordTypeFile);
            if (recordType?.RecordType.picklistValues) {
                const updatedPicklistValues = recordType.RecordType.picklistValues.filter((picklistValue) => {
                    return picklistValue?.picklist[0] !== fld;
                });
                if (updatedPicklistValues.length !== recordType.RecordType.picklistValues.length) {
                    recordType.RecordType.picklistValues = updatedPicklistValues;
                    await writeXmlFile(recordTypeFile, recordType);
                    uxLog("log", this, c.grey(`Cleaned file ${recordTypeFile} from ${obj}.${fld}`));
                }
            }
        }
    }
}
//# sourceMappingURL=references.js.map