UNPKG

@etm-professional-control/winccoa-mcp-server

Version:

MCP Server for WinCC OA with field-specific configurations

2,382 lines 130 kB
/**
 * OPC UA Connection Manager
 *
 * Provides functionality to create, configure, and browse OPC UA connections in WinCC OA.
 */
import { BaseConnection } from './BaseConnection.js';
import { DpConfigType, OpcUaDatatype, DpAddressDirection } from '../../types/index.js';
import { PmonClient } from '../pmon/PmonClient.js';
import { ManagerState } from '../../types/pmon/protocol.js';
// Re-export enums and constants for backward compatibility
export { SecurityPolicy, MessageSecurityMode, OPCUA_DEFAULTS } from '../../types/index.js';
/**
 * OPC UA Connection Manager Class
 *
 * Extends BaseConnection with OPC UA-specific functionality.
 */
export class OpcUaConnection extends BaseConnection {
    constructor() {
        super(...arguments);
        /**
         * Cache for browse results
         * Key format: `${connectionName}:${nodeId}:${eventSource}:${depth}`
         */
        this.browseCache = new Map();
        /**
         * Default TTL for cache entries (5 minutes in milliseconds)
         */
        this.DEFAULT_CACHE_TTL = 5 * 60 * 1000;
        /**
         * Maximum number of nodes to return in a single browse operation
         * Protects against context window overflow on client side
         * Set to 800 for optimal context usage with smart auto-depth
         */
        this.MAX_NODE_COUNT = 800;
        /**
         * Browse operation timeout in milliseconds
         * Prevents indefinite hangs on problematic browse operations
         */
        this.BROWSE_TIMEOUT_MS = 120000; // 120 seconds (2 minutes)
        /**
         * Maximum cache size in bytes (50MB)
         * Prevents unbounded cache growth
         */
        this.MAX_CACHE_SIZE_BYTES = 50 * 1024 * 1024;
    }
    /**
     * Generate a unique browse request ID
     * @returns Unique request ID
     */
    generateBrowseRequestId() {
        return `browse_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
    }
    /**
     * Generate cache key for browse results
     * @param connectionName - Connection name
     * @param nodeId - Node ID
     * @param eventSource - Event source type
     * @param depth - Browse depth
     * @returns Cache key
     */
    generateBrowseCacheKey(connectionName, nodeId, eventSource, depth) {
        return `${connectionName}:${nodeId}:${eventSource}:${depth}`;
    }
    /**
     * Get cached browse results if available and not expired
     * @param key - Cache key
     * @returns Cached nodes or undefined
     */
    getCachedBrowseResults(key) {
        const entry = this.browseCache.get(key);
        if (!entry) {
            return undefined;
        }
        const now = Date.now();
        if (now - entry.timestamp > entry.ttl) {
            // Entry expired, remove it
            this.browseCache.delete(key);
            return undefined;
        }
        console.log(`✓ Cache HIT for ${key}`);
        return entry.nodes;
    }
    /**
     * Cache browse results
     * @param key - Cache key
     * @param nodes - Browse nodes to cache
     * @param ttl - Time to live in milliseconds (optional, defaults to DEFAULT_CACHE_TTL)
     */
    cacheBrowseResults(key, nodes, ttl) {
        this.browseCache.set(key, {
            nodes,
            timestamp: Date.now(),
            ttl: ttl ?? this.DEFAULT_CACHE_TTL
        });
        console.log(`✓ Cached ${nodes.length} nodes for ${key} (TTL: ${ttl ?? this.DEFAULT_CACHE_TTL}ms)`);
    }
    /**
     * Clear expired cache entries (automatic cleanup)
     */
    cleanupExpiredCache() {
        const now = Date.now();
        let removedCount = 0;
        for (const [key, entry] of this.browseCache.entries()) {
            if (now - entry.timestamp > entry.ttl) {
                this.browseCache.delete(key);
                removedCount++;
            }
        }
        if (removedCount > 0) {
            console.log(`✓ Cleaned up ${removedCount} expired cache entries`);
        }
    }
    /**
     * Clear all cache entries for a specific connection
     * @param connectionName - Connection name
     */
    clearConnectionCache(connectionName) {
        let removedCount = 0;
        for (const key of this.browseCache.keys()) {
            if (key.startsWith(`${connectionName}:`)) {
                this.browseCache.delete(key);
                removedCount++;
            }
        }
        if (removedCount > 0) {
            console.log(`✓ Cleared ${removedCount} cache entries for connection ${connectionName}`);
        }
    }
    /**
     * Generate a unique connection name for OPC UA
     * @returns Connection name in format _OpcUAConnection<n>
     */
    async generateConnectionName() {
        return super.generateConnectionName('_OpcUAConnection');
    }
    /**
     * Ensure that the _OPCUA<managerNumber> datapoint exists
     * @param managerNumber - Manager number
     * @returns true on success
     */
    async ensureOpcUaManagerDpExists(managerNumber) {
        try {
            const dpName = `_OPCUA${managerNumber}`;
            if (this.checkDpExists(dpName)) {
                console.log(`Manager datapoint ${dpName} already exists`);
                return true;
            }
            console.log(`Creating manager datapoint ${dpName} of type _OPCUA`);
            const created = await this.winccoa.dpCreate(dpName, '_OPCUA');
            if (!created) {
                console.error(`Failed to create manager datapoint ${dpName}`);
                return false;
            }
            console.log(`Successfully created manager datapoint ${dpName}`);
            return true;
        }
        catch (error) {
            console.error(`Error ensuring manager datapoint exists:`, error);
            return false;
        }
    }
    /**
     * Validate the OPC UA connection configuration
     * @param config - Configuration to validate
     * @throws Error on invalid configuration
     */
    validateConnectionConfig(config) {
        // Validate IP address
        if (!this.validateIpAddress(config.ipAddress)) {
            throw new Error(`Invalid IP address or hostname: ${config.ipAddress}`);
        }
        // Validate port
        if (!this.validatePort(config.port)) {
            throw new Error(`Invalid port number: ${config.port}. Must be between 1 and 65535`);
        }
        // Validate manager number
        if (!this.validateManagerNumber(config.managerNumber)) {
            throw new Error(`Invalid manager number: ${config.managerNumber}. Must be between 1 and 99`);
        }
        // Validate reconnect timer
        if (config.reconnectTimer !== undefined && config.reconnectTimer <= 0) {
            throw new Error('Reconnect timer must be positive');
        }
    }
    /**
     * Validate that the OPC UA driver exists, is running, and has the correct manager number configured
     * @param managerNumber - The manager number to validate (e.g., 4 for _OPCUA4)
     * @returns Validation result with status, optional error message, and optional warnings
     */
    async validateOpcUaDriver(managerNumber) {
        const warnings = [];
        try {
            console.log(`🔍 Validating OPC UA driver for manager number ${managerNumber}...`);
            // 1. Ensure the manager datapoint exists (create if necessary)
            const managerDp = `_OPCUA${managerNumber}`;
            if (!this.checkDpExists(managerDp)) {
                console.log(`⚠️  Manager datapoint ${managerDp} does not exist`);
                console.log(`🔧 Creating manager datapoint ${managerDp}...`);
                try {
                    const created = await this.winccoa.dpCreate(managerDp, '_OPCUA');
                    if (!created) {
                        console.error(`❌ Failed to create manager datapoint ${managerDp}`);
                        return {
                            valid: false,
                            error: `OPC UA Manager datapoint ${managerDp} does not exist and could not be created automatically. Please create it manually using dpCreate("${managerDp}", "_OPCUA").`
                        };
                    }
                    console.log(`✅ Successfully created manager datapoint ${managerDp}`);
                    warnings.push(`Manager datapoint ${managerDp} was automatically created.`);
                }
                catch (createError) {
                    const errorMsg = createError instanceof Error ? createError.message : String(createError);
                    console.error(`❌ Error creating manager datapoint: ${errorMsg}`);
                    return {
                        valid: false,
                        error: `Failed to create manager datapoint ${managerDp}: ${errorMsg}. Please create it manually.`
                    };
                }
            }
            else {
                console.log(`✓ Manager datapoint ${managerDp} exists`);
            }
            // 1b. Ensure the _Driver{num} datapoint exists (required by WinCC OA driver infrastructure)
            const driverDp = `_Driver${managerNumber}`;
            if (!this.checkDpExists(driverDp)) {
                console.log(`🔧 Creating driver common datapoint ${driverDp}...`);
                try {
                    const createdDriver = await this.winccoa.dpCreate(driverDp, '_DriverCommon');
                    if (!createdDriver) {
                        console.warn(`⚠️  Failed to create driver common datapoint ${driverDp}`);
                        warnings.push(`Could not create driver common datapoint ${driverDp}.`);
                    }
                    else {
                        console.log(`✅ Successfully created driver common datapoint ${driverDp}`);
                        warnings.push(`Driver common datapoint ${driverDp} was automatically created.`);
                    }
                }
                catch (createDriverError) {
                    const errorMsg = createDriverError instanceof Error ? createDriverError.message : String(createDriverError);
                    console.warn(`⚠️  Error creating driver common datapoint: ${errorMsg}`);
                    warnings.push(`Could not create driver common datapoint ${driverDp}: ${errorMsg}`);
                }
            }
            else {
                console.log(`✓ Driver common datapoint ${driverDp} exists`);
            }
            // 2. Connect to Pmon and get manager status
            const pmonClient = new PmonClient();
            let status;
            let managerList;
            try {
                status = await pmonClient.getManagerStatus();
                managerList = await pmonClient.getManagerList();
                console.log(`✓ Connected to Pmon, found ${status.managers.length} managers`);
            }
            catch (pmonError) {
                const errorMsg = pmonError instanceof Error ? pmonError.message : String(pmonError);
                console.warn(`⚠️  Could not connect to Pmon: ${errorMsg}`);
                warnings.push(`Could not verify driver status via Pmon: ${errorMsg}. ` +
                    `Connection may fail if driver is not running. ` +
                    `Please ensure the OPC UA driver with '-num ${managerNumber}' is configured and running.`);
                return {
                    valid: true,
                    warnings
                };
            }
            // 3. First, check if the requested manager number is already in use by ANY driver
            const usedManagerNumbers = new Set();
            for (let i = 0; i < status.managers.length; i++) {
                const mgr = status.managers[i];
                if (!mgr)
                    continue;
                const mgrDetails = managerList[mgr.index];
                if (!mgrDetails)
                    continue;
                const commandLineStr = mgrDetails.commandlineOptions || '';
                // Extract -num parameter, default to 1 if not specified
                const numMatch = commandLineStr.match(/-num\s+(\d+)/);
                const configuredNum = numMatch && numMatch[1] ? parseInt(numMatch[1], 10) : 1;
                usedManagerNumbers.add(configuredNum);
            }
            console.log(`Currently used manager numbers: ${Array.from(usedManagerNumbers).sort((a, b) => a - b).join(', ')}`);
            // Check if requested manager number is already taken
            if (usedManagerNumbers.has(managerNumber)) {
                // Find which driver is using this number
                let conflictingDriver = null;
                for (let i = 0; i < status.managers.length; i++) {
                    const mgr = status.managers[i];
                    if (!mgr)
                        continue;
                    const mgrDetails = managerList[mgr.index];
                    if (!mgrDetails)
                        continue;
                    const commandLineStr = mgrDetails.commandlineOptions || '';
                    const numMatch = commandLineStr.match(/-num\s+(\d+)/);
                    const configuredNum = numMatch && numMatch[1] ? parseInt(numMatch[1], 10) : 1;
                    if (configuredNum === managerNumber) {
                        const managerNameStr = mgrDetails.manager || 'unknown';
                        const isOpcUaDriver = managerNameStr.toLowerCase().includes('opcua') ||
                            managerNameStr.toLowerCase().includes('opc-ua');
                        if (isOpcUaDriver) {
                            // It's an OPC UA driver - we can use it!
                            conflictingDriver = null;
                            break;
                        }
                        else {
                            // It's a different driver type
                            conflictingDriver = managerNameStr;
                            if (!numMatch) {
                                conflictingDriver += ' (running without -num parameter, implicitly uses -num 1)';
                            }
                        }
                    }
                }
                if (conflictingDriver) {
                    console.error(`❌ Manager number ${managerNumber} is already in use by: ${conflictingDriver}`);
                    return {
                        valid: false,
                        error: `Manager number ${managerNumber} is already in use by another driver: ${conflictingDriver}.\n\n` +
                            `Currently used manager numbers: ${Array.from(usedManagerNumbers).sort((a, b) => a - b).join(', ')}\n\n` +
                            `Please choose a different manager number (1-99) that is not already in use.`
                    };
                }
            }
            // 4. Search for OPC UA driver with the correct manager number
            let driverFound = false;
            let driverRunning = false;
            let driverIndex = null;
            let driverName = '';
            for (let i = 0; i < status.managers.length; i++) {
                const mgr = status.managers[i];
                if (!mgr)
                    continue;
                const mgrDetails = managerList[mgr.index];
                if (!mgrDetails)
                    continue;
                // Check if this is an OPC UA driver (look for OPCUA in the manager name)
                const isOpcUaDriver = mgrDetails.manager?.toLowerCase().includes('opcua') ||
                    mgrDetails.manager?.toLowerCase().includes('opc-ua');
                if (isOpcUaDriver) {
                    const managerNameStr = mgrDetails.manager || 'unknown';
                    const commandLineStr = mgrDetails.commandlineOptions || '';
                    console.log(`  Found OPC UA manager: ${managerNameStr}, options: "${commandLineStr}"`);
                    // Check if the manager number matches (look for "-num X" in command line options)
                    // IMPORTANT: Drivers without -num parameter implicitly run as -num 1
                    const numMatch = commandLineStr.match(/-num\s+(\d+)/);
                    const configuredNum = numMatch && numMatch[1] ? parseInt(numMatch[1], 10) : 1; // Default to 1 if no -num specified
                    if (configuredNum === managerNumber) {
                        driverFound = true;
                        driverIndex = mgr.index;
                        driverName = managerNameStr;
                        driverRunning = (mgr.state === ManagerState.Running);
                        if (numMatch) {
                            console.log(`✓ Found matching OPC UA driver '${driverName}' at index ${driverIndex} with -num ${managerNumber}`);
                        }
                        else {
                            console.log(`✓ Found matching OPC UA driver '${driverName}' at index ${driverIndex} (running as -num 1 by default, no -num specified)`);
                        }
                        console.log(`  Driver state: ${driverRunning ? 'RUNNING' : 'NOT RUNNING'} (state code: ${mgr.state})`);
                        break;
                    }
                }
            }
            // 5. Evaluate results and auto-create driver if missing
            if (!driverFound) {
                console.log(`❌ No OPC UA driver with '-num ${managerNumber}' found`);
                console.log(`🔧 Attempting to automatically create OPC UA driver...`);
                try {
                    // Try different manager names for different WinCC OA versions
                    const managerNames = ['WCCOAopcua', 'WCCOAopcuadrv'];
                    let addedSuccessfully = false;
                    let usedManagerName = '';
                    let usedPosition = 0;
                    for (const managerName of managerNames) {
                        // Find a free position for the new manager (after existing managers)
                        // Try to find the highest index and add after it
                        let maxIndex = 0;
                        for (const mgr of status.managers) {
                            if (mgr && mgr.index > maxIndex) {
                                maxIndex = mgr.index;
                            }
                        }
                        const nextPosition = maxIndex + 1;
                        console.log(`🔧 Trying to add manager '${managerName}' at position ${nextPosition}...`);
                        // Add the OPC UA driver using PmonClient with 'once' start option
                        const addResult = await pmonClient.addManager(nextPosition, managerName, 'once', 30, 3, 5, `-num ${managerNumber}`);
                        if (addResult.success) {
                            console.log(`✅ Successfully added OPC UA driver '${managerName}' at position ${nextPosition}`);
                            addedSuccessfully = true;
                            usedManagerName = managerName;
                            usedPosition = nextPosition;
                            break;
                        }
                        else {
                            console.warn(`⚠️  Failed to add manager '${managerName}': ${addResult.error}`);
                            // Try next manager name
                        }
                    }
                    if (!addedSuccessfully) {
                        console.error(`❌ Failed to add OPC UA driver with any manager name`);
                        return {
                            valid: false,
                            error: `No OPC UA driver with '-num ${managerNumber}' found and automatic creation failed.\n\n` +
                                `Tried manager names: ${managerNames.join(', ')}\n\n` +
                                `Please add the driver manually via WinCC OA Console:\n` +
                                `  1. Open Console and go to Para -> Distributed Systems -> Managers\n` +
                                `  2. Add new manager: ${managerNames[0]}\n` +
                                `  3. Options: -num ${managerNumber}\n` +
                                `  4. Start mode: always\n` +
                                `  5. Apply and start the manager`
                        };
                    }
                    console.log(`✅ Manager '${usedManagerName}' added to Pmon at position ${usedPosition}`);
                    // Wait a moment for Pmon to process
                    await new Promise(resolve => setTimeout(resolve, 1000));
                    // Verify the manager was actually added by refreshing the status
                    console.log(`🔍 Verifying manager was added to Pmon...`);
                    const verifyStatus = await pmonClient.getManagerStatus();
                    const verifyList = await pmonClient.getManagerList();
                    let verified = false;
                    for (let i = 0; i < verifyStatus.managers.length; i++) {
                        const mgr = verifyStatus.managers[i];
                        if (!mgr)
                            continue;
                        const mgrDetails = verifyList[mgr.index];
                        if (!mgrDetails)
                            continue;
                        if (mgrDetails.manager === usedManagerName &&
                            mgrDetails.commandlineOptions?.includes(`-num ${managerNumber}`)) {
                            verified = true;
                            usedPosition = mgr.index;
                            console.log(`✅ Verified: Manager '${usedManagerName}' is in Pmon at index ${usedPosition}`);
                            break;
                        }
                    }
                    if (!verified) {
                        console.error(`❌ Manager was reported as added but cannot be found in Pmon`);
                        warnings.push(`OPC UA driver '${usedManagerName}' was added to Pmon configuration but verification failed. ` +
                            `Please check WinCC OA Console to verify the manager exists and start it manually if needed.`);
                        return {
                            valid: true,
                            warnings
                        };
                    }
                    // Try to start the newly created driver
                    console.log(`🔧 Attempting to start the OPC UA driver at index ${usedPosition}...`);
                    const startResult = await pmonClient.startManager(usedPosition);
                    if (startResult.success) {
                        console.log(`✅ Successfully started OPC UA driver '${usedManagerName}' at index ${usedPosition}`);
                        warnings.push(`OPC UA driver '${usedManagerName}' was automatically created and started at position ${usedPosition}. ` +
                            `The driver is now running and ready for connections.`);
                    }
                    else {
                        console.warn(`⚠️  Driver created but failed to start: ${startResult.error}`);
                        warnings.push(`OPC UA driver '${usedManagerName}' was automatically created at position ${usedPosition} but could not be started. ` +
                            `Error: ${startResult.error}. ` +
                            `Please start it manually using WinCC OA Console. ` +
                            `The connection will work after starting the driver.`);
                    }
                    // Driver was created, continue with success
                    return {
                        valid: true,
                        warnings: warnings.length > 0 ? warnings : undefined
                    };
                }
                catch (createError) {
                    const createErrorMsg = createError instanceof Error ? createError.message : String(createError);
                    console.error(`❌ Error creating OPC UA driver:`, createErrorMsg);
                    return {
                        valid: false,
                        error: `No OPC UA driver with '-num ${managerNumber}' found and automatic creation failed.\n\n` +
                            `Error: ${createErrorMsg}\n\n` +
                            `Please add the driver manually:\n` +
                            `  1. Open WinCC OA Console -> Para -> Distributed Systems -> Managers\n` +
                            `  2. Add manager: WCCOAopcua (or WCCOAopcuadrv)\n` +
                            `  3. Options: -num ${managerNumber}\n` +
                            `  4. Start mode: always\n` +
                            `  5. Apply and start the driver`
                    };
                }
            }
            if (!driverRunning) {
                console.log(`⚠️  Driver found but not running (index ${driverIndex})`);
                console.log(`🔧 Attempting to start the OPC UA driver...`);
                try {
                    const startResult = await pmonClient.startManager(driverIndex);
                    if (startResult.success) {
                        console.log(`✅ Successfully started OPC UA driver '${driverName}' at index ${driverIndex}`);
                        warnings.push(`OPC UA driver '${driverName}' (index ${driverIndex}) was not running and has been automatically started. ` +
                            `The driver is now ready for connections.`);
                    }
                    else {
                        console.warn(`⚠️  Failed to start driver: ${startResult.error}`);
                        warnings.push(`OPC UA driver '${driverName}' (index ${driverIndex}) exists with '-num ${managerNumber}' ` +
                            `but is not running and could not be started automatically. ` +
                            `Error: ${startResult.error}. ` +
                            `Please start it manually using WinCC OA Console. ` +
                            `The connection will work after starting the driver.`);
                    }
                }
                catch (startError) {
                    const startErrorMsg = startError instanceof Error ? startError.message : String(startError);
                    console.error(`❌ Error starting driver:`, startErrorMsg);
                    warnings.push(`OPC UA driver '${driverName}' (index ${driverIndex}) exists but is not running. ` +
                        `Automatic start failed: ${startErrorMsg}. ` +
                        `Please start it manually using WinCC OA Console.`);
                }
            }
            console.log(`✓ OPC UA driver validation completed successfully`);
            return {
                valid: true,
                warnings: warnings.length > 0 ? warnings : undefined
            };
        }
        catch (error) {
            const errorMessage = error instanceof Error ? error.message : String(error);
            console.error(`❌ Error during driver validation:`, errorMessage);
            // If validation fails due to unexpected error, warn but don't block
            warnings.push(`Driver validation encountered an error: ${errorMessage}. ` +
                `Proceeding with connection creation, but please verify the driver configuration manually.`);
            return {
                valid: true,
                warnings
            };
        }
    }
    /**
     * Add server to running OPC UA driver using AddServer command
     * This allows the connection to be available immediately without driver restart
     * @param managerNumber - Manager number
     * @param connectionName - Connection name (WITHOUT leading underscore)
     * @returns true on success, false on failure (but logs warning only)
     */
    async addServerToRunningDriver(managerNumber, connectionName) {
        try {
            const managerDpName = `_OPCUA${managerNumber}`;
            const cmdAddServer = `${managerDpName}.Command.AddServer`;
            // Connection name without leading underscore
            const nameWithoutUnderscore = connectionName.startsWith('_')
                ? connectionName.substring(1)
                : connectionName;
            // Check if the Command.AddServer datapoint element exists
            if (!this.checkDpExists(managerDpName)) {
                console.warn(`Manager datapoint ${managerDpName} does not exist, skipping AddServer command`);
                return false;
            }
            console.log(`Triggering AddServer command for connection ${nameWithoutUnderscore} on running driver ${managerDpName}`);
            // Trigger AddServer command with connection name
            // This command adds the server to the running driver without restart
            await this.winccoa.dpSetWait(cmdAddServer, nameWithoutUnderscore);
            console.log(`✓ Successfully triggered AddServer command for ${nameWithoutUnderscore}`);
            return true;
        }
        catch (error) {
            // Don't fail the entire operation if AddServer command fails
            // The connection is already registered in Config.Servers, so it will work after driver restart
            console.warn(`Warning: Could not trigger AddServer command (driver may not be running or command not available):`, error);
            console.warn(`Connection will be available after driver restart`);
            return false;
        }
    }
    /**
     * Register the connection with the OPC UA manager
     * @param managerNumber - Manager number
     * @param connectionName - Connection name (WITHOUT leading underscore)
     * @returns true on success
     */
    async registerConnectionWithManager(managerNumber, connectionName) {
        try {
            const managerDpName = `_OPCUA${managerNumber}`;
            // Connection name without leading underscore
            const nameWithoutUnderscore = connectionName.startsWith('_')
                ? connectionName.substring(1)
                : connectionName;
            // Get current server list
            const currentServersRaw = await this.winccoa.dpGet(`${managerDpName}.Config.Servers`);
            const currentServers = Array.isArray(currentServersRaw) ? currentServersRaw : [];
            // Check if connection is already registered
            if (currentServers.includes(nameWithoutUnderscore)) {
                console.log(`Connection ${nameWithoutUnderscore} already registered with manager ${managerNumber}`);
                return true;
            }
            // Add connection to the list
            currentServers.push(nameWithoutUnderscore);
            console.log(`Registering connection ${nameWithoutUnderscore} with manager ${managerNumber}`);
            await this.winccoa.dpSetWait(`${managerDpName}.Config.Servers`, currentServers);
            console.log(`Successfully registered connection ${nameWithoutUnderscore}`);
            // Trigger AddServer command to add server to running driver (if available)
            // This eliminates the need for driver restart
            await this.addServerToRunningDriver(managerNumber, nameWithoutUnderscore);
            return true;
        }
        catch (error) {
            console.error(`Error registering connection with manager:`, error);
            return false;
        }
    }
    /**
     * Configure the OPC UA connection
     * @param config - Connection configuration
     * @param connectionName - Connection name
     * @returns true on success
     */
    async configureConnection(config, connectionName) {
        try {
            const serverUrl = `opc.tcp://${config.ipAddress}:${config.port}`;
            // Get defaults from imported constant
            const defaults = await import('../../types/index.js').then(m => m.OPCUA_DEFAULTS);
            // Basic configuration
            const dpes = [
                `${connectionName}.Config.ConnInfo`,
                `${connectionName}.Config.AccessInfo`,
                `${connectionName}.Config.Password`,
                `${connectionName}.Config.Security.Policy`,
                `${connectionName}.Config.Security.MessageMode`,
                `${connectionName}.Config.Security.Certificate`,
                `${connectionName}.Config.Active`,
                `${connectionName}.Config.ReconnectTimer`,
                `${connectionName}.Config.Separator`,
                `${connectionName}.Config.Flags`,
                `${connectionName}.Redu.Config.ConnInfo`,
                `${connectionName}.Redu.Config.Active`
            ];
            const values = [
                serverUrl, // ConnInfo
                config.username || '', // AccessInfo (Username)
                config.password ? Buffer.from(config.password, 'utf-8') : Buffer.alloc(0), // Password (blob type)
                config.securityPolicy ?? defaults.securityPolicy, // Security Policy
                config.messageSecurityMode ?? defaults.messageSecurityMode, // Message Mode
                config.clientCertificate || '', // Client Certificate
                (config.enableConnection ?? defaults.enableConnection) ? 1 : 0, // Active
                config.reconnectTimer ?? defaults.reconnectTimer, // ReconnectTimer
                config.separator ?? defaults.separator, // Separator
                0, // Flags (default)
                'opc.tcp://', // Redu.ConnInfo (empty)
                0 // Redu.Active (inactive)
            ];
            console.log(`Configuring connection ${connectionName}:`);
            console.log(`- Server URL: ${serverUrl}`);
            console.log(`- Authentication: ${config.username ? 'Username/Password' : 'Anonymous'}`);
            console.log(`- Reconnect Timer: ${config.reconnectTimer ?? defaults.reconnectTimer} seconds`);
            // Set the configuration
            await this.winccoa.dpSetWait(dpes, values);
            console.log(`Successfully configured connection ${connectionName}`);
            return true;
        }
        catch (error) {
            console.error(`Error configuring connection:`, error);
            return false;
        }
    }
    /**
     * Browse a single level (depth=1) for size estimation
     * Used to check address space size before allowing deep browsing
     *
     * @param connDp - Connection datapoint name (with _ prefix)
     * @param nodeId - Node ID to browse
     * @param eventSource - Event source type
     * @returns Promise with array of direct children nodes
     */
    async browseSingleLevel(connDp, nodeId, eventSource) {
        try {
            const requestId = this.generateBrowseRequestId();
            const nodeCount = await new Promise((resolve, reject) => {
                const timeout = setTimeout(() => {
                    reject(new Error('Timeout during size estimation'));
                }, 10000); // 10 second timeout
                const callback = async () => {
                    try {
                        const values = await this.winccoa.dpGet([
                            `${connDp}.Browse.RequestId`,
                            `${connDp}.Browse.DisplayNames`
                        ]);
                        const returnedRequestId = values[0];
                        if (returnedRequestId !== requestId) {
                            return; // Not our request
                        }
                        const displayNames = values[1];
                        const count = displayNames ? displayNames.filter(n => n && n.length > 0).length : 0;
                        clearTimeout(timeout);
                        this.winccoa.dpDisconnect(connId);
                        resolve(count);
                    }
                    catch (error) {
                        clearTimeout(timeout);
                        this.winccoa.dpDisconnect(connId);
                        reject(error);
                    }
                };
                const connId = this.winccoa.dpConnect(callback, [`${connDp}.Browse.DisplayNames`, `${connDp}.Browse.RequestId`], false);
                this.winccoa
                    .dpSetWait(`${connDp}.Browse.GetBranch:_original.._value`, [requestId, nodeId, 1, eventSource])
                    .catch(reject);
            });
            return nodeCount;
        }
        catch (error) {
            console.error('Error estimating address space size:', error);
            return 0; // Return 0 on error to allow browsing to proceed
        }
    }
    /**
     * Build branch estimates from browse results
     * Analyzes flat browse results to identify large branches
     *
     * @param nodes - Flat array of browse nodes
     * @param parentNodeId - Parent node that was browsed
     * @returns Array of branch information for branches with > 100 estimated children
     */
    buildBranchEstimates(nodes, parentNodeId) {
        const branchMap = new Map();
        // Parse browsePaths to identify parent-child relationships
        // BrowsePath format: "/0:Objects/2:DeviceSet/3:IoT_Suite_Mobile/3:Counters"
        for (const node of nodes) {
            // Skip nodes without browsePath (shouldn't happen but be safe)
            if (!node.nodeId)
                continue;
            // For depth=1 browse, all nodes are direct children - count them
            // For depth>1, we need to parse browsePaths to group by parent
            // Simple heuristic: If node is Object/Folder type, estimate children count
            // based on how many nodes appear after this one in the hierarchy
            const isContainer = node.nodeClass?.includes('Object') || node.nodeClass?.includes('Folder');
            if (isContainer) {
                branchMap.set(node.nodeId, {
                    count: 1, // Will be updated if we find children
                    node: node
                });
            }
        }
        // Build branch info for branches with significant child counts
        const largeBranches = [];
        for (const [nodeId, info] of branchMap.entries()) {
            // For depth=1 results, we can't estimate children accurately
            // Mark as "potentially large" if it's a container type
            if (info.node.hasChildren) {
                largeBranches.push({
                    nodeId: nodeId,
                    displayName: info.node.displayName,
                    estimatedChildren: 0, // Unknown for depth=1
                    level: 1,
                    browsePath: nodeId
                });
            }
        }
        return largeBranches;
    }
    /**
     * Browse with automatic depth selection (smart auto-depth)
     * Tries depth=2 first, retries with depth=1 if result exceeds 800 nodes
     * Uses 'auto' cache key to maximize cache hit rate
     *
     * @param connectionName - Connection name
     * @param parentNodeId - Parent node ID to browse
     * @param eventSource - Event source type
     * @param useCache - Use cached results
     * @param refreshCache - Force refresh cache
     * @param maxNodes - Maximum nodes to return (default: 800)
     * @returns Browse result with actualDepthUsed metadata
     */
    async browseWithAutoDepth(connectionName, parentNodeId, eventSource, useCache, refreshCache, maxNodes = 800) {
        // Check cache with 'auto' key first
        const autoCacheKey = this.generateBrowseCacheKey(connectionName, parentNodeId, eventSource, 'auto');
        if (useCache && !refreshCache) {
            const cachedResults = this.getCachedBrowseResults(autoCacheKey);
            if (cachedResults) {
                console.log(`✓ Auto-depth cache HIT for ${autoCacheKey}`);
                return {
                    nodes: cachedResults.slice(0, maxNodes),
                    isPartial: cachedResults.length > maxNodes,
                    totalNodes: cachedResults.length,
                    offset: 0,
                    limit: maxNodes,
                    hasMore: cachedResults.length > maxNodes,
                    nextOffset: cachedResults.length > maxNodes ? maxNodes : null,
                    actualDepthUsed: 2 // Assume cached results were depth=2
                };
            }
        }
        console.log(`Auto-depth browse: Trying depth=2 first...`);
        try {
            // Try depth=2 first (temporarily disable smart depth limiting)
            const result = await this.browse(connectionName, parentNodeId, eventSource, 2, // depth=2
            useCache, refreshCache, maxNodes, 0, // offset
            maxNodes // limit
            );
            // If result has more than maxNodes nodes, retry with depth=1
            if (result.totalNodes && result.totalNodes > maxNodes) {
                console.log(`Auto-depth: depth=2 returned ${result.totalNodes} nodes (> ${maxNodes}), retrying with depth=1...`);
                const result1 = await this.browse(connectionName, parentNodeId, eventSource, 1, // depth=1
                useCache, refreshCache, maxNodes, 0, maxNodes);
                // Add metadata showing we auto-downgraded
                result1.actualDepthUsed = 1;
                result1.warning = (result1.warning || '') +
                    ` Auto-adjusted from depth=2 to depth=1 to stay under ${maxNodes}-node limit. ` +
                    `Address space is large. Browse specific nodes for deeper levels.`;
                // Identify large branches
                if (result1.nodes.length > 0) {
                    result1.largeBranches = this.buildBranchEstimates(result1.nodes, parentNodeId);
                }
                // Cache result with 'auto' key
                this.cacheBrowseResults(autoCacheKey, result1.nodes);
                return result1;
            }
            // depth=2 fit under limit, return it
            console.log(`Auto-depth: depth=2 returned ${result.totalNodes || result.nodes.length} nodes, within limit`);
            result.actualDepthUsed = 2;
            // Cache result with 'auto' key
            this.cacheBrowseResults(autoCacheKey, result.nodes);
            return result;
        }
        catch (error) {
            // If depth=2 failed due to smart depth limiting, fall back to depth=1
            const errorMsg = error instanceof Error ? error.message : String(error);
            if (errorMsg.includes('depth=2') || errorMsg.includes('Address space is large')) {
                console.log(`Auto-depth: depth=2 rejected by smart limiting, using depth=1...`);
                const result1 = await this.browse(connectionName, parentNodeId, eventSource, 1, useCache, refreshCache, maxNodes, 0, maxNodes);
                result1.actualDepthUsed = 1;
                result1.warning = (result1.warning || '') +
                    ` Auto-adjusted to depth=1 due to large address space. Browse specific nodes for deeper levels.`;
                // Identify large branches
                if (result1.nodes.length > 0) {
                    result1.largeBranches = this.buildBranchEstimates(result1.nodes, parentNodeId);
                }
                // Cache result with 'auto' key
                this.cacheBrowseResults(autoCacheKey, result1.nodes);
                return result1;
            }
            // Other error, rethrow
            throw error;
        }
    }
    /**
     * Selective recursive browse (Option C)
     * Pre-checks depth=1, then selectively browses children to stay under maxNodes
     * Maximizes depth within node budget
     *
     * @param connectionName - Connection name
     * @param parentNodeId - Parent node ID to browse
     * @param eventSource - Event source type
     * @param useCache - Use cached results
     * @param refreshCache - Force refresh cache
     * @param maxNodes - Maximum total nodes to return (default: 800)
     * @returns Browse result with expandableBranches metadata
     */
    async selectiveBrowse(connectionName, parentNodeId, eventSource, useCache, refreshCache, maxNodes = 800) {
        console.log(`Selective browse: Starting with depth=1 pre-check...`);
        // Step 1: Browse depth=1 to get direct children
        const level1Result = await this.browse(connectionName, parentNodeId, eventSource, 1, useCache, refreshCache, maxNodes, 0, maxNodes);
        const directChildren = level1Result.nodes;
        const directChildCount = directChildren.length;
        console.log(`Selective browse: Found ${directChildCount} direct children`);
        // If depth=1 already exceeds budget, return as-is
        if (directChildCount >= maxNodes) {
            console.log(`Selective browse: depth=1 already at/over budget, returning level 1 only`);
            level1Result.actualDepthUsed = 1;
            level1Result.warning = `Address space has ${directChildCount} direct children. ` +
                `Showing first ${maxNodes}. Browse specific nodes for deeper levels.`;
            return level1Result;
        }
        // Step 2: Calculate budget for expanding children
        const remainingBudget = maxNodes - directChildCount;
        console.log(`Selective browse: ${remainingBudget} nodes remaining for expansions`);
        // Step 3: Identify expandable children (Objects/Folders with hasChildren=true)
        const expandableBranches = directChildren.filter(node => node.hasChildren === true);
        if (expandableBranches.length === 0) {
            console.log(`Selective browse: No expandable branches found, returning depth=1`);
            level1Result.actualDepthUsed = 1;
            return level1Result;
        }
        console.log(`Selective browse: Found ${expandableBranches.length} expandable branches`);
        // Step 4: Try to expand all branches with depth=2 if budget allows
        // Estimate: each branch might have ~10-20 children average
        const estimatedPerBranch = Math.floor(remainingBudget / expandableBranches.length);
        if (estimatedPerBranch < 5) {
            // Not enough budget to expand meaningfully, return depth=1 with expandable info
            console.log(`Selective browse: Budget too tight (${estimatedPerBranch} per branch), returning depth=1 with guidance`);
            level1Result.actualDepthUsed = 1;
            level1Result.expandableBranches = expandableBranches.map(node => ({
                nodeId: node.nodeId,
                displayName: node.displayName,
                estimatedChildren: 0,
                level: 1
            }));
            level1Result.warning = `Showing ${directChildCount} nodes at depth=1. ` +
                `${expandableBranches.length} branches can be expanded. ` +
                `Browse specific branches individually: ${expandableBranches.slice(0, 3).map(n => n.displayName).join(', ')}${expandableBranches.length > 3 ? ', ...' : ''}`;
            return level1Result;
        }
        // Step 5: Expand selected branches
        console.log(`Selective browse: Expanding branches (budget: ${estimatedPerBranch} nodes per branch)...`);
        const allNodes = [...directChildren];
        const notExpandedBranches = [];
        let expandedCount = 0;
        for (const branch of expandableBranches) {
            // Check if we still have budget
            if (allNodes.length >= maxNodes) {
                // Out of budget, mark remaining branches as not expanded
                notExpandedBranches.push({
                    nodeId: branch.nodeId,
                    displayName: branch.displayName,
                    estimatedChildren: 0,
                    level: 1
                });
                continue;
            }
            try {
                // Browse this specific branch with depth=1 (its direct children)
                const branchResult = await this.browse(connectionName, branch.nodeId, eventSource, 1, useCache, refreshCache, maxNodes - allNodes.length, // Remaining budget
                0, maxNodes - allNodes.length);
                // Add children to result
                const childrenToAdd = branchResult.nodes.slice(0, maxNodes - allNodes.length);
                allNodes.push(...childrenToAdd);
                expandedCount++;
                console.log(`Selective browse: Expanded ${branch.displayName} (+${childrenToAdd.length} nodes, total: ${allNodes.length})`);
                // If we're at budget, stop
                if (allNodes.length >= maxNodes) {
                    // Mark remaining as not expanded
                    const currentIndex = expandableBranches.indexOf(branch);
                    for (let i = currentIndex + 1; i < expandableBranches.length; i++) {
                        const remainingBranch = expandableBranches[i];
                        if (remainingBranch) {
                            notExpandedBranches.push({
                                nodeId: remainingBranch.nodeId,
                                displayName: remainingBranch.displayName,
                                estimatedChildren: 0,
                                level: 1
                            });
                        }
                    }
                    break;
                }
            }
            catch (error) {
                console.error(`Selective browse: Failed to expand ${branch.displayName}:`, error);
                notExpandedBranches.push({
                    nodeId: branch.nodeId,
                    displayName: branch.displayName,
                    estimatedChildren: 0,
                    level: 1
                });
            }
        }
        // Build final result
        const result = {
            nodes: allNodes,
            isPartial: notExpandedBranches.length > 0,
            totalNodes: allNodes.length,
            actualDepthUsed: expandedCount > 0 ? 2 : 1,
            expandableBranches: notExpandedBranches.length > 0 ? notExpandedBranches : undefined,
            warning: notExpandedBranches.length > 0
                ? `Showing ${allNodes.length} nodes with selective expansion (${expandedCount}/${expandableBranches.length} branches expanded). ` +
                    `${notExpandedBranches.length} branches not expanded due to ${maxNodes}-node limit. ` +
                    `Browse these nodes individually: ${notExpandedBranches.slice(0, 3).map(b => b.displayName).join(', ')}${notExpandedBranches.length > 3 ? ', ...' : ''}`
                : `Showing ${allNodes.length} nodes with full expansion at depth=2`,
            offset: 0,
            limit: maxNodes,
            hasMore: false,
            nextOffset: null
        };
        console.log(`Selective browse: Complete. ${allNodes.length} total nodes, depth=${result.actualDepthUsed}, ${notExpandedBranches.length} branches not expanded`);
        return result;
    }
    /**
     * Estimate safe depth for batched browsing based on remaining budget
     * Uses hasChildren information and conservative estimates to avoid exceeding limits
     *
     * @param remainingBudget - Number of nodes remaining in budget
     * @param parentNode - Parent node being browsed (optional, for hasChildren hint)
     * @returns Recommended depth for next browse operation (1-3)
     */
    estimateSafeDepth(remainingBudget, parentNode) {
        // Use hasChildren hint if available
        // If parent has no children, no need to browse deeper
        if (parentNode && parentNode.hasChildren === false) {
            return 1; // Leaf node, no point going deeper
        }
        // Conservative depth estimation to avoid budget overrun
        // Formula: depth=N typically returns N levels * avgChildren^(N-1) nodes
        if (remainingBudget > 700) {
            // Plenty of budget: try depth=3
            // Estimate: ~100-200 nodes for typical hierarchy
            return 3;
        }
        if (remainingBudget > 400) {
            // Moderate budget: try depth=2
            // Estimate: ~20-50 nodes for typical hierarchy
            return 2;
        }
        if (remainingBudget > 100) {
            // Low budget: use depth=1
            // Estimate: ~5-15 nodes
            return 1;
        }
        // Very low budget: depth=1 but we're close to limit
        return 1;
    }
    /**
     * Check if OPC UA connection is established and ready for browsing
     * Uses Common.State.ConnState (unified driver state across all WinCC OA drivers)
     * @param connDp - Connection datapoint name (with _ prefix)
     * @throws Error if connection is not established (Common.State.ConnState < 256)
     */
    async checkConnectionEstablished(connDp) {
        try {
            // Check if connection datapoint exists
            const exists = this.winccoa.dpExists(connDp);
            if (!exists) {
                throw new Error(`OPC UA connection '${connDp}' does not exist. ` +
                    `Please create the connection first using the 'opcua-add-connection' tool.`);
            }
            // Read common connection state (unified across all drivers)
            const connStateResult = await this.winccoa.dpGet([`${connDp}.Common.State.ConnState`]);
            const connState = connStateResult[0];
            // Check if connected (state >= 256)
            // Per WinCC OA documentation: values < 256 = not connected, values >= 256 = connected
            if (connState < 256) {
                // Get server state for additional context
                let serverState = 'Unknown';
                try {
                    const serverStateResult = await this.winccoa.dpGet([`${connDp}.State.ServerState`]);
                    serverState = serverStateResult[0] || 'Unknown';
                }
                catch {
                    // Ignore error, server state is optional context
                }
                // State meanings for user reference (from WinCC OA Common.State.ConnState)
                const stateDescriptions = {
                    [-1]: 'Not Initialized',
                    0: 'Undefined',
                    1: 'Not Connected',
                    2: 'Connecting',
                    3: 'Not Active',
                    4: 'Disconnecting',
                    5: 'Failure',
                    9: 'WaitForReconnect'
                };
                const currentStateDesc = stateDescriptions[connState] || `Unknown state (${connState})`;
                throw new Error(`OPC UA connection '${connDp}' is not established.\n` +
                    `Connection state (Common.State.ConnState): ${connState} (${currentStateDesc})\n` +
                    `Server state: ${serverState}\n\n` +
                    `Please ensure:\n` +
                    `- The OPC UA connection is active\n` +
                    `- The OPC UA server is reachable\n` +
                    `- The OPC UA driver is running\n\n` +
                    `State meanings (Common.State.ConnState):\n` +
                    `- -1 = Not Initialized\n` +
                    `- 0 = Undefined\n` +
                    `- 1 = Not Connected\n` +
                    `- 2 = Connecting (please wait and retry)\n` +
                    `- 3 = Not Active\n` +
                    `- 4 = Disconnecting\n` +
                    `- 5 = Failure\n` +
                    `- 9 = WaitForReconnect\n` +
                    `- 256+ = Connected (ready to browse)`);
            }
            console.log(`Connection ${connDp} is established (Common.State.ConnState=${connState}, >= 256 = connected)`);
        }
        catch (error) {
            // Re-throw with context
            if (error instanceof Error) {
                throw error;
            }
            throw new Error(`Failed to check connection status for ${connDp}: ${String(error)}`);
        }
    }
    /**
     * Browse full branch recursively using depth-first exploration
     * Explores entire branch to leaf nodes, minimizing API calls by using batched depth
     *
     * @param connectionName - Connection name (with _ prefix)
     * @param startNodeId - Starting node ID for branch
     * @param eventSource - Event source type
     * @param softLimit - Soft limit for node count (default: 800, orientation)
     * @param hardLimit - Hard limit for node count (default: 1000, absolute maximum)
     * @param maxDepth - Maximum recursion depth (default: 10, safety limit)
     * @param useCache - Use cached results
     * @param refreshCache - Force refresh cache
     * @returns Browse result with full branch exploration
     */
    async browseFullBranch(connectionName, startNodeId, eventSource, softLimit = 800, hardLimit = 1000, maxDepth = 10, useCache = true, refreshCache = false) {
        console.log(`Full branch browse: Starting recursive exploration of ${startNodeId}`);
        console.log(`Limits: soft=${softLimit}, hard=${hardLimit}, maxDepth=${maxDepth}`);
        const allNodes = [];
        const exploredBranches = [];
        const expandableBranches = [];
        let totalApiCalls = 0;
        let maxDepthReached = 0;
        let leafNodesCount = 0;
        const nodesToExplore = [{
                nodeId: startNodeId,
                displayName: 'root',
                currentDepth: 0
            }];
        // Track which nodes we've already explored to avoid duplicates
        const exploredNodeIds = new Set();
        // Depth-first exploration loop
        while (nodesToExplore.length > 0 && allNodes.length < hardLimit) {
            // Pop from end (depth-first)
            const current = nodesToExplore.pop();
            // Skip if already explored
            if (exploredNodeIds.has(current.nodeId)) {
                continue;
            }
            // Check depth limit
            if (current.currentDepth >= maxDepth) {
                console.log(`Depth limit reached at ${current.displayName} (depth=${current.currentDepth})`);
                expandableBranches.push({
                    nodeId: current.nodeId,
                    displayName: current.displayName,
                    estimatedChildren: 0,
                    level: current.currentDepth
                });
                continue;
            }
            // Calculate safe batch depth for this browse operation
            const remainingBudget = hardLimit - allNodes.length;
            const batchDepth = this.estimateSafeDepth(remainingBudget);
            console.log(`Browsing ${current.displayName} (depth=${current.currentDepth}, batchDepth=${batchDepth}, remaining=${remainingBudget})`);
            try {
                // Browse this node with calculated batch depth
                const result = await this.browse(connectionName, current.nodeId, eventSource, batchDepth, useCache, refreshCache, hardLimit, 0, hardLimit);
                totalApiCalls++;
                exploredNodeIds.add(current.nodeId);
                // Add all returned nodes
                const newNodes = result.nodes || [];
                allNodes.push(...newNodes);
                console.log(`  → Got ${newNodes.length} nodes (total: ${allNodes.length})`);
                // Update max depth reached
                maxDepthReached = Math.max(maxDepthReached, current.currentDepth + batchDepth);
                // Check if we're approaching limits
                if (allNodes.length >= softLimit && allNodes.length < hardLimit) {
                    console.log(`Soft limit reached (${allNodes.length}/${softLimit}), will complete current branches`);
                }
                // Find nodes with children that need further exploration
                const nodesWithChildren = newNodes.filter(n => n.hasChildren === true);
                const leafNodes = newNodes.filter(n => n.hasChildren === false);
                leafNodesCount += leafNodes.length;
                console.log(`  → ${nodesWithChildren.length} expandable, ${leafNodes.length} leaves`);
                // If we have nodes with children and budget remaining, queue them for exploration
                if (nodesWithChildren.length > 0 && allNodes.length < hardLimit) {
                    // Add to stack (reverse order for depth-first left-to-right)
                    for (let i = nodesWithChildren.length - 1; i >= 0; i--) {
                        const node = nodesWithChildren[i];
                        if (!node)
                            continue; // Skip undefined nodes
                        // Check if this would exceed hard limit
                        if (allNodes.length + nodesToExplore.length >= hardLimit) {
                            expandableBranches.push({
                                nodeId: node.nodeId,
                                displayName: node.displayName,
                                estimatedChildren: 0,
                                level: current.currentDepth + 1
                            });
                        }
                        else {
                            nodesToExplore.push({
                                nodeId: node.nodeId,
                                displayName: node.displayName,
                                currentDepth: current.currentDepth + 1
                            });
                        }
                    }
                }
                // Mark branch as fully explored if we reached all leaves
                if (nodesWithChildren.length === 0 || allNodes.length >= hardLimit) {
                    exploredBranches.push(current.displayName);
                }
                // Hard limit check
                if (allNodes.length >= hardLimit) {
                    console.log(`Hard limit reached (${allNodes.length}/${hardLimit}), stopping exploration`);
                    // Mark remaining queued nodes as expandable
                    for (const remaining of nodesToExplore) {
                        expandableBranches.push({
                            nodeId: remaining.nodeId,
                            displayName: remaining.displayName,
                            estimatedChildren: 0,
                            level: remaining.currentDepth
                        });
                    }
                    break;
                }
            }
            catch (error) {
                console.error(`Error browsing ${current.displayName}:`, error);
                expandableBranches.push({
                    nodeId: current.nodeId,
                    displayName: current.displayName,
                    estimatedChildren: 0,
                    level: current.currentDepth
                });
            }
        }
        // Build final result
        const hitSoftLimit = allNodes.length >= softLimit;
        const hitHardLimit = allNodes.length >= hardLimit;
        let warning = '';
        if (hitHardLimit) {
            warning = `Hard limit reached (${allNodes.length}/${hardLimit} nodes). `;
        }
        else if (hitSoftLimit) {
            warning = `Soft limit reached (${allNodes.length}/${softLimit} nodes). `;
        }
        if (exploredBranches.length > 0) {
            warning += `Fully explored: ${exploredBranches.slice(0, 3).join(', ')}${exploredBranches.length > 3 ? ', ...' : ''}. `;
        }
        if (expandableBranches.length > 0) {
            warning += `Not explored (${expandableBranches.length} branches): ${expandableBranches.slice(0, 3).map(b => b.displayName).join(', ')}${expandableBranches.length > 3 ? ', ...' : ''}. Browse these individually.`;
        }
        const result = {
            nodes: allNodes,
            isPartial: expandableBranches.length > 0,
            totalNodes: allNodes.length,
            actualDepthUsed: maxDepthReached,
            exploredBranches: exploredBranches.length > 0 ? exploredBranches : undefined,
            expandableBranches: expandableBranches.length > 0 ? expandableBranches : undefined,
            recursionStats: {
                maxDepthReached,
                totalLevelsExplored: maxDepthReached,
                leafNodesReached: leafNodesCount,
                totalApiCalls
            },
            warning: warning || undefined,
            offset: 0,
            limit: hardLimit,
            hasMore: false,
            nextOffset: null
        };
        console.log(`Full branch browse complete: ${allNodes.length} nodes, ${totalApiCalls} API calls, max depth ${maxDepthReached}`);
        return result;
    }
    /**
     * Browse the OPC UA address space with smart auto-depth and pagination
     *
     * @param connectionName - Name of the connection (e.g., '_OpcUAConnection1')
     * @param parentNodeId - Node ID of the parent node (optional, default: "ns=0;i=85" for Objects folder)
     * @param eventSource - 0=Value (default), 1=Event, 2=Alarm&Condition
     * @param depth - Number of levels to browse (optional). If not specified, auto-selects depth=1 or 2 based on address space size.
     *                When specified: 1-5 allowed. User-specified depths are validated (rejected if would exceed 800 nodes).
     * @param useCache - Use cached results if available (default: true)
     * @param refreshCache - Force refresh cache (default: false)
     * @param maxNodeCount - Maximum nodes to return (default: 800). Prevents context overflow.
     * @param offset - Starting position for pagination (default: 0). Skip first N nodes.
     * @param limit - Max nodes per page (default: 800, max: 800). Use with offset for pagination.
     * @returns Promise with browse result including nodes and pagination metadata
     */
    async browse(connectionName, parentNodeId, eventSource = 0, depth, useCache = true, refreshCache = false, maxNodeCount, offset = 0, limit) {
        try {
            // Ensure connection name has leading underscore
            const connDp = connectionName.startsWith('_') ? connectionName : `_${connectionName}`;
            // Check if connection is established before browsing
            await this.checkConnectionEstablished(connDp);
            // Default to Objects folder if no parent specified
            const startNode = parentNodeId || 'ns=0;i=85';
            // Maximum nodes to return (default to 800, matching Option C requirement)
            const maxNodes = maxNodeCount || 800;
            // Apply pagination parameters
            const pageOffset = Math.max(0, offset); // Ensure non-negative
            const pageLimit = limit ? Math.min(limit, maxNodes) : maxNodes;
            // AUTO-DEPTH: If depth not specified, use smart auto-depth strategy
            if (depth === undefined) {
                // For root nodes (Objects folder), be conservative with auto-depth
                // For specific branches, be more aggressive (try up to depth=3)
                const isRootNode = startNode === 'ns=0;i=85' || startNode === 'ns=0;i=84' ||
                    startNode === 'ns=0;i=86' || startNode === 'ns=0;i=87';
                if (isRootNode) {
                    console.log(`Auto-depth browsing (root node) for ${startNode}`);
                    return await this.browseWithAutoDepth(connDp, startNode, eventSource, useCache, refreshCache, maxNodes);
                }
                else {
                    // Specific branch: browse entire branch recursively to leaf nodes
                    console.log(`Auto-depth browsing (specific branch) for ${startNode} - using full branch browse`);
                    return await this.browseFullBranch(connDp, startNode, eventSource, maxNodes, // soft limit (800)
                    1000, // hard limit
                    10, // max depth
                    useCache, refreshCache);
                }
            }
            // VALIDATE USER-SPECIFIED DEPTH: Ensure it won't cause overflow
            // Validate depth range (1-5 allowed, 0 disabled for safety)
            if (depth < 1 || depth > 5) {
                throw new Error(`Invalid depth ${depth}. Depth must be between 1 and 5. ` +
                    `depth=0 (unlimited browsing) is disabled for safety to prevent crashes on large address spaces. ` +
                    `Omit depth parameter to use smart auto-depth selection.`);
            }
            // Smart depth limiting: check address space size before allowing deep browse
            if (depth > 1 && !refreshCache) {
                console.log(`Checking address space size for depth=${depth} validation...`);
                const childCount = await this.browseSingleLevel(connDp, startNode, eventSource);
                console.log(`Found ${childCount} direct children at this level`);
                // If > 50 children and depth > 2, reject (would return 10K+ nodes)
                if (childCount > 50 && depth > 2) {
                    throw new Error(`Address space is large (${childCount} direct children). ` +
                        `depth=${depth} would likely return 10,000+ nodes causing context overflow (>200K tokens). ` +
                        `Maximum depth allowed for this node: 2. ` +
                        `Recommendation: Use depth=1 or depth=2, and browse incrementally with pagination.`);
                }
                // If > 100 children, only allow depth=1
                if (childCount > 100 && depth > 1) {
                    throw new Error(`Address space is very large (${childCount} direct children). ` +
                        `depth=${depth} would cause context overflow. ` +
                        `Only depth=1 allowed for this node. ` +
                        `Please browse incrementally: use depth=1 to get children, then browse each child individually.`);
                }
            }
            // Cleanup expired cache entries periodically
            this.cleanupExpiredCache();
            // Generate cache key
            const cacheKey = this.generateBrowseCacheKey(connDp, startNode, eventSource, depth);
            // Check cache if enabled and not refreshing
            if (useCache && !refreshCache) {
                const cachedFullResults = this.getCachedBrowseResults(cacheKey);
                if (cachedFullResults) {
                    console.log(`✓ Cache HIT for ${cacheKey} (${cachedFullResults.length} total nodes cached)`);
                    // Apply pagination to CACHED FULL results
                    const totalNodes = cachedFullResults.length;
                    const startIndex = pageOffset;
                    const endIndex = Math.min(startIndex + pageLimit, totalNodes);
                    const paginatedResults = cachedFullResults.slice(startIndex, endIndex);
                    // Calculate pagination metadata
                    const hasMore = endIndex < totalNodes;
                    const nextOffset = hasMore ? endIndex : null;
                    const isPartial = hasMore;
                    let warning;
                    if (isPartial) {
                        warning = `Showing nodes ${startIndex + 1}-${endIndex} of ${totalNodes} total (from cache). ` +
                            `Use offset=${nextOffset} to get the next page.`;
                    }
                    console.log(`Returning page ${startIndex}-${endIndex} from cache`);
                    // Return paginated cached results with full metadata
                    return {
                        nodes: paginatedResults,
                        isPartial,
                        warning,
                        appliedLimit: pageLimit,
                        totalNodes,
                        offset: startIndex,
                        limit: pageLimit,
                        hasMore,
                        nextOffset
                    };
                }
                console.log(`Cache MISS for ${cacheKey}`);
            }
            console.log(`Browsing ${depth} level(s) for node ${startNode}`);
            // Generate unique request ID
            const requestId = this.generateBrowseRequestId();
            // Perform browse operation with specified depth, timeout, and node limit
            const browseResult = await new Promise((resolve, reject) => {
                let timeoutId = null;
                let connId = null;
                let isCompleted = false;
                // Cleanup function to avoid memory leaks
                const cleanup = () => {
                    if (isCompleted)
                        return;
                    isCompleted = true;
                    if (timeoutId) {
                        clearTimeout(timeoutId);
                        timeoutId = null;
                    }
                    if (connId !== null) {
                        this.winccoa.dpDisconnect(connId);
                        connId = null;
                    }
                };
                // Setup timeout protection
                timeoutId = setTimeout(() => {
                    if (isCompleted)
                        return;
                    console.error(`Browse operation timed out after ${this.BROWSE_TIMEOUT_MS}ms`);
                    cleanup();
                    reject(new Error(`Browse operation timed out after ${this.BROWSE_TIMEOUT_MS / 1000} seconds (2 minutes). ` +
                        `This usually indicates a very large address space or connectivity issues. ` +
                        `Try browsing a more specific node, reducing the depth parameter, or checking server connectivity.`));
                }, this.BROWSE_TIMEOUT_MS);
                // Callback function for dpConnect
                const browseCallback = async (dpes) => {
                    try {
                        if (isCompleted)
                            return; // Already completed (timeout or previous call)
                        console.log(`Browse callback triggered`);
                        // Read all values at once with a single dpGet call
                        const values = await this.winccoa.dpGet([
                            `${connDp}.Browse.RequestId`,
                            `${connDp}.Browse.DisplayNames`,
                            `${connDp}.Browse.BrowsePaths`,
                            `${connDp}.Browse.NodeIds`,
                            `${connDp}.Browse.DataTypes`,
                            `${connDp}.Browse.ValueRanks`,
                            `${connDp}.Browse.NodeClasses`
                        ]);
                        const returnedRequestId = values[0];
                        const displayNames = values[1];
                        const browsePaths = values[2];
                        const nodeIds = values[3];
                        const dataTypes = values[4];
                        const valueRanks = values[5];
                        const nodeClasses = values[6];
                        // Check if this is our request
                        if (returnedRequestId !== requestId) {
                            console.log(`RequestId mismatch, ignoring callback`);
                            return; // Not our request, ignore
                        }
                        console.log(`RequestId matches, processing ${displayNames.length} nodes`);
                        // Build ALL results first (MINIMAL FIELDS ONLY)
                        const allResults = [];
                        for (let i = 0; i < displayNames.length; i++) {
                            const displayName = displayNames[i];
                            if (displayName && displayName.length > 0) {
                                const nodeClass = nodeClasses?.[i] || 'Unknown';
                                // Smart heuristic for hasChildren flag
                                // Objects and Folders typically have children, Variables typically don't
                                let hasChildren = undefined;
                                if (nodeClass.includes('Object') || nodeClass.includes('Folder')) {
                                    hasChildren = true;
                                }
                                else if (nodeClass.includes('Variable')) {
                                    hasChildren = false;
                                }
                                // For other node classes (Method, etc.), leave undefined
                                // MINIMAL RESPONSE: Only displayName, nodeId, nodeClass, hasChildren
                                // For full details (browsePath, dataType, valueRank, description, etc.),
                                // use opcua-get-node-details tool
                                allResults.push({
                                    displayName: displayName,
                                    nodeId: nodeIds[i] || '',
                                    nodeClass: nodeClass,
                                    hasChildren: hasChildren
                                });
                            }
                        }
                        // Apply pagination: slice results based on offset and limit
                        const totalNodes = allResults.length;
                        const startIndex = pageOffset;
                        const endIndex = Math.min(startIndex + pageLimit, totalNodes);
                        const paginatedResults = allResults.slice(startIndex, endIndex);
                        // Calculate pagination metadata
                        const hasMore = endIndex < totalNodes;
                        const nextOffset = hasMore ? endIndex : null;
                        const isPartial = hasMore;
                        let warning;
                        if (isPartial) {
                            warning = `Showing nodes ${startIndex + 1}-${endIndex} of ${totalNodes} total. ` +
                                `Use offset=${nextOffset} to get the next page.`;
                        }
                        console.log(`Browse completed: ${paginatedResults.length} nodes returned (${startIndex}-${endIndex} of ${totalNodes})`);
                        // Cleanup and resolve with pagination metadata
                        cleanup();
                        resolve({
                            nodes: paginatedResults,
                            isPartial,
                            warning,
                            appliedLimit: pageLimit,
                            totalNodes,
                            offset: startIndex,
                            limit: pageLimit,
                            hasMore,
                            nextOffset,
                            _fullResults: allResults // Internal field for caching (not exposed to client)
                        });
                    }
                    catch (error) {
                        console.error(`Error in browse callback:`, error);
                        cleanup();
                        reject(error);
                    }
                };
                // Connect callback to browse datapoints
                connId = this.winccoa.dpConnect(browseCallback, [
                    `${connDp}.Browse.DisplayNames`,
                    `${connDp}.Browse.BrowsePaths`,
                    `${connDp}.Browse.NodeIds`,
                    `${connDp}.Browse.DataTypes`,
                    `${connDp}.Browse.ValueRanks`,
                    `${connDp}.Browse.NodeClasses`,
                    `${connDp}.Browse.RequestId`
                ], false // Don't send initial values
                );
                // Trigger browse request - pass depth directly to WinCC OA
                this.winccoa
                    .dpSetWait(`${connDp}.Browse.GetBranch:_original.._value`, [requestId, startNode, depth, eventSource])
                    .catch((error) => {
                    cleanup();
                    reject(error);
                });
            });
            // Cache FULL results (not paginated) for future pagination requests
            if (useCache && browseResult._fullResults) {
                const fullResults = browseResult._fullResults;
                const estimatedSize = JSON.stringify(fullResults).length;
                // Only cache if total size < 5MB
                if (estimatedSize < this.MAX_CACHE_SIZE_BYTES / 10) {
                    const cacheKey = this.generateBrowseCacheKey(connDp, startNode, eventSource, depth);
                    this.cacheBrowseResults(cacheKey, fullResults);
                    console.log(`✓ Cached ${fullResults.length} FULL nodes for ${cacheKey} (all pages can use this cache)`);
                }
                else {
                    console.log(`⚠ Skipping cache: Result size (${Math.round(estimatedSize / 1024 / 1024)}MB) exceeds 5MB limit`);
                }
                // Remove internal field before returning to client
                delete browseResult._fullResults;
            }
            return browseResult;
        }
        catch (error) {
            console.error(`Error browsing OPC UA connection:`, error);
            throw error;
        }
    }
    /**
     * Get detailed node information for specific nodes (batch operation)
     * COMMENTED OUT: NodeDetails type was removed along with opcua_node_details tool
     * Can be re-enabled if needed by adding back NodeDetails type definition
     *
     * @param connectionName - Name of the connection (e.g., '_OpcUAConnection1')
     * @param nodeIds - Array of node IDs to fetch details for
     * @returns Promise with array of node details
     */
    /*
    async getNodeDetails(
      connectionName: string,
      nodeIds: string[]
    ): Promise<NodeDetails[]> {
      try {
        // Ensure connection name has leading underscore
        const connDp = connectionName.startsWith('_') ? connectionName : `_${connectionName}`;
  
        console.log(`Fetching details for ${nodeIds.length} nodes from ${connDp}`);
  
        // Validate node IDs
        if (!nodeIds || nodeIds.length === 0) {
          throw new Error('nodeIds array is empty. Please provide at least one node ID.');
        }
  
        const results: NodeDetails[] = [];
  
        // Process each node ID (batch operation)
        for (const nodeId of nodeIds) {
          try {
            // Generate unique request ID for this node
            const requestId = this.generateBrowseRequestId();
  
            // Browse this specific node at depth=0 to get its full details
            // This gives us all the attributes we need
            const nodeInfo = await new Promise<NodeDetails>((resolve, reject) => {
              const timeout = setTimeout(() => {
                reject(new Error(`Timeout fetching details for node ${nodeId}`));
              }, 10000); // 10 second timeout per node
  
              // Callback function for dpConnect
              const detailsCallback = async (dpes: string[]) => {
                try {
                  // Read all values at once
                  const values = await this.winccoa.dpGet([
                    `${connDp}.Browse.RequestId`,
                    `${connDp}.Browse.DisplayNames`,
                    `${connDp}.Browse.BrowsePaths`,
                    `${connDp}.Browse.NodeIds`,
                    `${connDp}.Browse.DataTypes`,
                    `${connDp}.Browse.ValueRanks`,
                    `${connDp}.Browse.NodeClasses`
                  ]) as any[];
  
                  const returnedRequestId = values[0] as string;
                  if (returnedRequestId !== requestId) {
                    return; // Not our request
                  }
  
                  const displayNames = values[1] as string[];
                  const browsePaths = values[2] as string[];
                  const nodeIds = values[3] as string[];
                  const dataTypes = values[4] as string[];
                  const valueRanks = values[5] as string[];
                  const nodeClasses = values[6] as string[];
  
                  // Should have exactly one result (the node itself)
                  if (displayNames && displayNames.length > 0 && displayNames[0]) {
                    clearTimeout(timeout);
                    this.winccoa.dpDisconnect(connId);
  
                    resolve({
                      nodeId: nodeId,
                      displayName: displayNames[0] || '',
                      browsePath: browsePaths[0] || '',
                      nodeClass: nodeClasses[0] || 'Unknown',
                      dataType: dataTypes[0] || '',
                      valueRank: valueRanks[0] || ''
                    });
                  } else {
                    clearTimeout(timeout);
                    this.winccoa.dpDisconnect(connId);
                    reject(new Error(`Node ${nodeId} not found or has no data`));
                  }
                } catch (error) {
                  clearTimeout(timeout);
                  this.winccoa.dpDisconnect(connId);
                  reject(error);
                }
              };
  
              // Connect callback
              const connId = this.winccoa.dpConnect(
                detailsCallback,
                [
                  `${connDp}.Browse.DisplayNames`,
                  `${connDp}.Browse.BrowsePaths`,
                  `${connDp}.Browse.NodeIds`,
                  `${connDp}.Browse.DataTypes`,
                  `${connDp}.Browse.ValueRanks`,
                  `${connDp}.Browse.NodeClasses`,
                  `${connDp}.Browse.RequestId`
                ],
                false
              );
  
              // Trigger browse for this specific node (depth=0, eventSource=0)
              this.winccoa
                .dpSetWait(`${connDp}.Browse.GetBranch:_original.._value`, [requestId, nodeId, 0, 0])
                .catch(reject);
            });
  
            results.push(nodeInfo);
            console.log(`✓ Fetched details for node: ${nodeInfo.displayName} (${nodeId})`);
          } catch (error) {
            // If individual node fails, add error entry but continue with others
            const errorMessage = error instanceof Error ? error.message : String(error);
            console.error(`✗ Failed to fetch details for node ${nodeId}:`, errorMessage);
            results.push({
              nodeId: nodeId,
              displayName: '',
              browsePath: '',
              nodeClass: '',
              dataType: '',
              valueRank: '',
              error: errorMessage
            });
          }
        }
  
        console.log(`Fetched details for ${results.length} nodes (${results.filter(r => !r.error).length} successful)`);
        return results;
      } catch (error) {
        console.error(`Error fetching node details:`, error);
        throw error;
      }
    }
    */
    /**
     * Get manager number for a given OPC UA connection
     * Searches for which _OPCUA{num} manager has this connection registered
     *
     * @param connectionName - Connection name (normalized with _ prefix)
     * @returns Manager number (1-255)
     * @throws Error if no manager found
     */
    async getManagerNumberForConnection(connectionName) {
        try {
            // Normalize connection name (remove leading underscore for search)
            const normalizedName = connectionName.startsWith('_')
                ? connectionName.substring(1)
                : connectionName;
            console.log(`Auto-detecting manager for connection: ${normalizedName}`);
            // 1. Try to find which _OPCUA{num} has this connection registered
            // Get all _OPCUA{num} datapoints
            const opcuaManagers = this.winccoa.dpNames('_OPCUA*', '_OPCUA');
            for (const managerDp of opcuaManagers) {
                try {
                    const servers = await this.winccoa.dpGet(`${managerDp}.Config.Servers`);
                    if (Array.isArray(servers) && servers.includes(normalizedName)) {
                        // Extract number from "_OPCUA{num}"
                        const match = managerDp.match(/_OPCUA(\d+)/);
                        if (match && match[1]) {
                            const managerNum = parseInt(match[1]);
                            console.log(`Found connection registered with ${managerDp}`);
                            return managerNum;
                        }
                    }
                }
                catch (error) {
                    // Skip managers that don't have Config.Servers or are not accessible
                    continue;
                }
            }
            // 2. Fallback: Find first running OPCUA driver by checking _Driver* datapoints
            const drivers = this.winccoa.dpNames('_Driver*', '_DriverCommon');
            for (const driverDp of drivers) {
                try {
                    const driverType = await this.winccoa.dpGet(`${driverDp}.DT`);
                    if (driverType === "OPCUAC") { // OPC UA Client driver type
                        const match = driverDp.match(/_Driver(\d+)/);
                        if (match && match[1]) {
                            const driverNum = parseInt(match[1]);
                            console.log(`Found running OPC UA driver with number ${driverNum}`);
                            return driverNum;
                        }
                    }
                }
                catch (error) {
                    continue;
                }
            }
            throw new Error(`No OPC UA manager found for connection ${connectionName}. ` +
                `Please ensure: 1) Connection is registered with a manager (_OPCUA{num}.Config.Servers), ` +
                `or 2) An OPC UA driver is running.`);
        }
        catch (error) {
            const errorMessage = error instanceof Error ? error.message : String(error);
            throw new Error(`Failed to detect manager number: ${errorMessage}`);
        }
    }
    /**
     * Validate manager number and check if connection is registered with it
     *
     * @param managerNumber - Manager number to validate
     * @param connectionName - Connection name (normalized with _ prefix)
     * @throws Error if validation fails
     */
    async validateManagerNumberForConnection(managerNumber, connectionName) {
        // 1. Validate range
        if (managerNumber < 1 || managerNumber > 255) {
            throw new Error(`Manager number ${managerNumber} out of valid range (1-255)`);
        }
        // 2. Check if manager datapoint exists
        const managerDp = `_OPCUA${managerNumber}`;
        if (!this.checkDpExists(managerDp)) {
            throw new Error(`OPC UA Manager ${managerDp} does not exist. ` +
                `Please create it first or check your manager number.`);
        }
        // 3. Check if connection is registered with this manager
        const normalizedName = connectionName.startsWith('_')
            ? connectionName.substring(1)
            : connectionName;
        try {
            const servers = await this.winccoa.dpGet(`${managerDp}.Config.Servers`);
            if (!Array.isArray(servers) || !servers.includes(normalizedName)) {
                throw new Error(`Connection ${connectionName} is not registered with manager ${managerDp}. ` +
                    `Registered connections: ${servers && servers.length > 0 ? servers.join(', ') : 'none'}`);
            }
            console.log(`Validated: Connection ${connectionName} is registered with ${managerDp}`);
        }
        catch (error) {
            const errorMessage = error instanceof Error ? error.message : String(error);
            throw new Error(`Failed to validate manager registration: ${errorMessage}`);
        }
    }
    /**
     * Ensure that a poll group exists for polling or spontaneous mode
     *
     * Based on reference implementation IT_OT_BL.ctl lines 1014-1041:
     * - Uses _PollGroup type (not _OPCUASubscription)
     * - For polling mode: Uses PollInterval to control how often data is queried
     * - For spontaneous mode: Just needs a name in _poll_group attribute
     * - Poll group can be shared across multiple datapoints
     *
     * @param subscriptionName - Poll group/subscription name (e.g., '_DefaultSubscription' or 'DefaultSubscription')
     * @param connectionName - Connection name (normalized with _ prefix)
     * @returns Poll group name with _ prefix
     * @throws Error if poll group cannot be created
     */
    async ensureSubscriptionExists(subscriptionName, connectionName) {
        try {
            // Normalize subscription name (ensure _ prefix)
            const normalizedSub = subscriptionName.startsWith('_')
                ? subscriptionName
                : `_${subscriptionName}`;
            // Check if subscription/poll group already exists
            const subscriptionExists = this.checkDpExists(normalizedSub);
            if (subscriptionExists) {
                console.log(`Poll group/subscription ${normalizedSub} already exists`);
                return normalizedSub;
            }
            // Create as _PollGroup (used for both polling and spontaneous)
            // For polling mode: PollInterval controls how often data is queried
            // For spontaneous mode: Just needs a name reference in _poll_group attribute
            console.log(`Creating poll group ${normalizedSub} of type _PollGroup`);
            const created = await this.winccoa.dpCreate(normalizedSub, '_PollGroup');
            if (!created) {
                throw new Error(`Failed to create poll group ${normalizedSub}`);
            }
            // Configure poll group settings
            // Active: Enable the poll group
            // PollInterval: Controls polling frequency (1000ms = 1 second)
            console.log(`Configuring poll group ${normalizedSub}:`);
            console.log(`  - Active: true`);
            console.log(`  - PollInterval: 1000ms`);
            await this.winccoa.dpSetWait([
                `${normalizedSub}.Active`,
                `${normalizedSub}.PollInterval`
            ], [
                1, // Active = true
                1000 // Poll interval in ms (1 second)
            ]);
            console.log(`✓ Successfully created and configured poll group ${normalizedSub}`);
            return normalizedSub;
        }
        catch (error) {
            const errorMessage = error instanceof Error ? error.message : String(error);
            throw new Error(`Failed to ensure subscription exists: ${errorMessage}`);
        }
    }
    /**
     * Build OPC UA reference string
     * Format: ConnectionName$$1$1$NodeId (note the DOUBLE $$)
     *
     * IMPORTANT: Based on reference implementation IT_OT_BL.ctl line 1048:
     * - Use DOUBLE dollar signs $$ between connection and mode
     * - Do NOT include subscription name in reference string
     * - Subscription is specified separately in _poll_group attribute
     *
     * @param connectionName - Connection name (with _ prefix, e.g., '_OpcUAConnection1')
     * @param nodeId - OPC UA Node ID (e.g., 'ns=2;s=MyVariable')
     * @returns Formatted reference string
     */
    buildReferenceString(connectionName, nodeId) {
        // Remove leading underscore for reference string
        const conn = connectionName.startsWith('_')
            ? connectionName.substring(1)
            : connectionName;
        // Format: Connection$$Variant$Mode$NodeId
        // Note: DOUBLE $$ is critical!
        // Variant: 1 = NodeId (not browse path)
        // Mode: 1 = NodeId format
        return `${conn}$$1$1$${nodeId}`;
    }
    /**
     * Configure address and distribution settings for an OPC UA datapoint element
     * Sets both _address and _distrib configs with OPC UA-specific parameters
     *
     * @param dpName - Full datapoint element name (e.g., 'MyDatapoint.Value')
     * @param connectionName - OPC UA connection name (with or without _ prefix)
     * @param reference - OPC UA NodeId reference (e.g., 'ns=2;s=MyVariable')
     * @param datatype - OPC UA transformation type (750-768, default: 750=DEFAULT)
     * @param direction - Address direction mode (0-15, default: 4=INPUT_POLL)
     * @param active - Activate address immediately (default: true)
     * @param managerNumber - Optional manager number (1-255). If not specified, auto-detected.
     * @param subscription - Optional poll group name. If not specified, uses '_DefaultPollingFast'.
     * @returns true on success
     * @throws Error with detailed message on failure
     */
    async addAddressConfig(dpName, connectionName, reference, datatype = OpcUaDatatype.DEFAULT, direction = DpAddressDirection.DPATTR_ADDR_MODE_INPUT_POLL, active = true, managerNumber, subscription = 'DefaultPollingFast') {
        try {
            console.log(`Configuring OPC UA address for ${dpName}`);
            // 1. Validate datapoint exists
            const dpBaseName = dpName.split('.')[0];
            if (!dpBaseName || !this.checkDpExists(dpBaseName)) {
                throw new Error(`Datapoint ${dpName} does not exist. Please create it first.`);
            }
            // 2. Normalize connectionName (ensure _ prefix)
            const normalizedConnection = connectionName.startsWith('_')
                ? connectionName
                : `_${connectionName}`;
            // 3. Validate connection exists
            if (!this.checkDpExists(normalizedConnection)) {
                throw new Error(`OPC UA connection ${normalizedConnection} does not exist. ` +
                    `Available connections: ${this.winccoa.dpNames('_OpcUAConnection*', '_OPCUAServer').join(', ')}`);
            }
            // 4. Get manager number: explicit or auto-detect
            let finalManagerNumber;
            if (managerNumber !== undefined) {
                await this.validateManagerNumberForConnection(managerNumber, normalizedConnection);
                finalManagerNumber = managerNumber;
                console.log(`Using explicitly specified manager number: ${finalManagerNumber}`);
            }
            else {
                finalManagerNumber = await this.getManagerNumberForConnection(normalizedConnection);
                console.log(`Auto-detected manager number: ${finalManagerNumber}`);
            }
            // 5. Validate datatype (750-768 for OPC UA)
            if (datatype < 750 || datatype > 768) {
                throw new Error(`Invalid OPC UA datatype ${datatype}. Must be between 750 and 768.\n` +
                    `Common values:\n` +
                    `  750 = DEFAULT (automatic detection)\n` +
                    `  751 = BOOLEAN\n` +
                    `  756 = INT32\n` +
                    `  760 = FLOAT\n` +
                    `  762 = STRING\n` +
                    `See OpcUaDatatype enum for full list.`);
            }
            // 6. Validate direction (0-15)
            if (direction < 0 || direction > 15) {
                throw new Error(`Invalid address direction ${direction}. Must be between 0 and 15.\n` +
                    `Common values:\n` +
                    `  4 = INPUT_POLL (polled input, default)\n` +
                    `  2 = INPUT_SPONT (spontaneous input)\n` +
                    `  1 = OUTPUT (output)\n` +
                    `See DpAddressDirection enum for full list.`);
            }
            // 7. Ensure subscription exists (create if necessary)
            const normalizedSubscription = await this.ensureSubscriptionExists(subscription, normalizedConnection);
            // 8. Build proper reference string (NO subscription in reference!)
            //    Subscription is specified in _poll_group attribute
            const fullReference = this.buildReferenceString(normalizedConnection, reference);
            // 9. Build DpAddressConfig with ALL required fields
            const addressConfig = {
                _type: DpConfigType.DPCONFIG_PERIPH_ADDR_MAIN,
                _drv_ident: "OPCUA",
                // NOTE: _connection is intentionally NOT set for OPC UA (based on working config)
                _reference: fullReference, // Full reference (single $ separators)
                _direction: direction,
                _datatype: datatype, // CRITICAL: Transformation type
                _subindex: 0, // Always 0 for OPC UA
                _internal: false,
                _lowlevel: true,
                _offset: 0, // No offset by default
                _poll_group: normalizedSubscription, // Poll group name (used for both polling and spontaneous modes)
                _active: active // Set active in the initial config
            };
            // 10. Build DpDistribConfig
            const distribConfig = {
                _type: DpConfigType.DPCONFIG_DISTRIBUTION_INFO,
                _driver: finalManagerNumber
            };
            console.log(`\n╔════════════════════════════════════════════════════════════════╗`);
            console.log(`║ OPC UA Address Configuration for: ${dpName.padEnd(30)} ║`);
            console.log(`╠════════════════════════════════════════════════════════════════╣`);
            console.log(`  Connection: ${normalizedConnection}`);
            console.log(`  Poll Group: ${normalizedSubscription}`);
            console.log(`  Full Reference: ${fullReference}`);
            console.log(`  Original NodeId: ${reference}`);
            console.log(`  Datatype: ${datatype} (CRITICAL FIELD)`);
            console.log(`  Direction: ${direction}`);
            console.log(`  Manager: ${finalManagerNumber}`);
            console.log(`╠════════════════════════════════════════════════════════════════╣`);
            console.log(`║ Complete addressConfig object:                                 ║`);
            console.log(`╚════════════════════════════════════════════════════════════════╝`);
            console.log(JSON.stringify(addressConfig, null, 2));
            console.log(`\n╔════════════════════════════════════════════════════════════════╗`);
            console.log(`║ Complete distribConfig object:                                 ║`);
            console.log(`╚════════════════════════════════════════════════════════════════╝`);
            console.log(JSON.stringify(distribConfig, null, 2));
            console.log();
            // 11. Set BOTH _address and _distrib configs in a SINGLE ATOMIC operation
            //     CRITICAL: WinCC OA requires these to be set together!
            const configSuccess = await this.setAddressAndDistribConfig(dpName, addressConfig, distribConfig);
            if (!configSuccess) {
                throw new Error('Failed to set _address and _distrib configuration atomically');
            }
            console.log(`✓ Successfully configured OPC UA address for ${dpName}`);
            return true;
        }
        catch (error) {
            const errorMessage = error instanceof Error ? error.message : String(error);
            console.error(`✗ Error configuring OPC UA address for ${dpName}:`, errorMessage);
            throw error; // Re-throw for detailed error in MCP Tool
        }
    }
    /**
     * Create and configure an OPC UA client connection
     *
     * @param config - Configuration of the OPC UA connection
     * @returns Connection name on success, throws Error on failure
     */
    async addConnection(config) {
        try {
            console.log('========================================');
            console.log('Starting OPC UA Connection Setup');
            console.log('========================================');
            // Validate configuration
            this.validateConnectionConfig(config);
            console.log('✓ Configuration validated');
            // Validate OPC UA driver existence and status
            const driverValidation = await this.validateOpcUaDriver(config.managerNumber);
            if (!driverValidation.valid) {
                throw new Error(driverValidation.error);
            }
            if (driverValidation.warnings && driverValidation.warnings.length > 0) {
                console.log('========================================');
                console.log('⚠️  Driver Validation Warnings:');
                console.log('========================================');
                driverValidation.warnings.forEach(warning => {
                    console.warn(`⚠️  ${warning}`);
                });
                console.log('========================================');
            }
            console.log('✓ OPC UA driver validated');
            // Auto-generate connection name
            const connectionName = await this.generateConnectionName();
            console.log(`✓ Connection name: ${connectionName}`);
            // Ensure that _OPCUA<managerNumber> exists
            const managerDpCreated = await this.ensureOpcUaManagerDpExists(config.managerNumber);
            if (!managerDpCreated) {
                throw new Error('Failed to ensure manager datapoint exists');
            }
            console.log(`✓ Manager datapoint _OPCUA${config.managerNumber} ready`);
            // Create connection datapoint
            const connectionDpCreated = await this.ensureConnectionDpExists(connectionName, '_OPCUAServer');
            if (!connectionDpCreated) {
                throw new Error('Failed to create connection datapoint');
            }
            console.log(`✓ Connection datapoint ${connectionName} ready`);
            // Configure connection
            const configured = await this.configureConnection(config, connectionName);
            if (!configured) {
                throw new Error('Failed to configure connection');
            }
            console.log(`✓ Connection configured`);
            // Register connection with manager
            const registered = await this.registerConnectionWithManager(config.managerNumber, connectionName);
            if (!registered) {
                throw new Error('Failed to register connection with manager');
            }
            console.log(`✓ Connection registered with _OPCUA${config.managerNumber}`);
            console.log('========================================');
            console.log('✓ OPC UA Connection Setup Complete');
            console.log(`  Connection: ${connectionName}`);
            console.log(`  Server: opc.tcp://${config.ipAddress}:${config.port}`);
            console.log(`  Manager: _OPCUA${config.managerNumber}`);
            console.log('========================================');
            return connectionName;
        }
        catch (error) {
            console.error('========================================');
            console.error('✗ OPC UA Connection Setup Failed');
            console.error('========================================');
            console.error(`Error: ${error}`);
            throw error;
        }
    }
    /**
     * Delete an OPC UA connection
     *
     * Completely removes an OPC UA connection by:
     * 1. Removing it from the manager's server list (_OPCUA{num}.Config.Servers)
     * 2. Deleting the connection datapoint
     * 3. If no connections remain on the driver:
     *    - Stops the OPC UA driver
     *    - Removes the driver from Pmon
     *    - Deletes the _OPCUA{num} manager datapoint
     * 4. Cleans up any other unused _OPCUA{num} datapoints (those with empty server lists)
     *
     * This provides complete cleanup - the entire OPC UA infrastructure is removed
     * if no longer needed, including orphaned manager datapoints.
     *
     * Note: The driver may need to be restarted for the changes to take full effect,
     * or will reload automatically if configured to do so.
     *
     * @param connectionName - Name of the connection to delete (with or without _ prefix)
     * @param managerNumber - Optional manager number. If not specified, will be auto-detected.
     * @returns true on success, throws Error on failure
     */
    async deleteConnection(connectionName, managerNumber) {
        try {
            console.log('========================================');
            console.log('Starting OPC UA Connection Deletion');
            console.log('========================================');
            // Normalize connection name (ensure _ prefix)
            const normalizedConnection = connectionName.startsWith('_')
                ? connectionName
                : `_${connectionName}`;
            console.log(`Connection to delete: ${normalizedConnection}`);
            // Check if connection exists
            const connectionExists = this.checkDpExists(normalizedConnection);
            if (!connectionExists) {
                console.warn(`⚠️  Connection ${normalizedConnection} does not exist`);
                console.log(`Available connections: ${this.winccoa.dpNames('_OpcUAConnection*', '_OPCUAServer').join(', ')}`);
                console.log(`Will proceed with cleanup of unused manager datapoints and drivers...`);
            }
            // Get manager number if not provided (only if connection exists)
            let finalManagerNumber;
            let managerDp;
            if (connectionExists) {
                if (managerNumber !== undefined) {
                    finalManagerNumber = managerNumber;
                    console.log(`Using provided manager number: ${finalManagerNumber}`);
                }
                else {
                    finalManagerNumber = await this.getManagerNumberForConnection(normalizedConnection);
                    console.log(`Auto-detected manager number: ${finalManagerNumber}`);
                }
                managerDp = `_OPCUA${finalManagerNumber}`;
                // Connection name without leading underscore
                const nameWithoutUnderscore = normalizedConnection.startsWith('_')
                    ? normalizedConnection.substring(1)
                    : normalizedConnection;
                // 1. Remove connection from manager's server list
                console.log(`Removing connection from ${managerDp}.Config.Servers...`);
                try {
                    const currentServersRaw = await this.winccoa.dpGet(`${managerDp}.Config.Servers`);
                    const currentServers = Array.isArray(currentServersRaw) ? currentServersRaw : [];
                    const updatedServers = currentServers.filter(s => s !== nameWithoutUnderscore);
                    if (updatedServers.length === currentServers.length) {
                        console.warn(`⚠️  Connection ${nameWithoutUnderscore} was not found in server list`);
                    }
                    else {
                        await this.winccoa.dpSetWait(`${managerDp}.Config.Servers`, updatedServers);
                        console.log(`✓ Removed connection from server list`);
                    }
                }
                catch (error) {
                    console.warn(`⚠️  Could not update server list:`, error);
                    // Continue with deletion anyway
                }
                // 2. Delete the connection datapoint
                // Note: The connection will be removed from the driver when it's restarted,
                // or immediately if the driver reloads its configuration automatically
                console.log(`Deleting connection datapoint ${normalizedConnection}...`);
                try {
                    const deleted = await this.winccoa.dpDelete(normalizedConnection);
                    if (!deleted) {
                        throw new Error(`dpDelete returned false for ${normalizedConnection}`);
                    }
                    console.log(`✓ Deleted connection datapoint ${normalizedConnection}`);
                }
                catch (error) {
                    const errorMessage = error instanceof Error ? error.message : String(error);
                    throw new Error(`Failed to delete connection datapoint: ${errorMessage}`);
                }
            }
            else {
                console.log(`Skipping connection deletion as it does not exist`);
            }
            // 3. Check if there are any remaining connections on this driver (only if manager was identified)
            let remainingServers = [];
            let driverRemovedSuccessfully = false;
            if (managerDp && finalManagerNumber !== undefined) {
                console.log(`Checking for remaining connections on ${managerDp}...`);
                try {
                    const remainingServersRaw = await this.winccoa.dpGet(`${managerDp}.Config.Servers`);
                    remainingServers = Array.isArray(remainingServersRaw) ? remainingServersRaw : [];
                    if (remainingServers.length === 0) {
                        console.log(`⚠️  No more connections on ${managerDp}, removing driver...`);
                        // Get the driver's manager index from Pmon
                        const pmonClient = new PmonClient();
                        let driverIndex = null;
                        try {
                            const status = await pmonClient.getManagerStatus();
                            const managerList = await pmonClient.getManagerList();
                            for (let i = 0; i < status.managers.length; i++) {
                                const mgr = status.managers[i];
                                if (!mgr)
                                    continue;
                                const mgrDetails = managerList[mgr.index];
                                if (!mgrDetails)
                                    continue;
                                // Check if this is the OPC UA driver with the correct number
                                const isOpcUaDriver = mgrDetails.manager?.toLowerCase().includes('opcua') ||
                                    mgrDetails.manager?.toLowerCase().includes('opc-ua');
                                if (isOpcUaDriver) {
                                    // IMPORTANT: Drivers without -num parameter implicitly run as -num 1
                                    const numMatch = mgrDetails.commandlineOptions?.match(/-num\s+(\d+)/);
                                    const configuredNum = numMatch && numMatch[1] ? parseInt(numMatch[1], 10) : 1; // Default to 1 if no -num specified
                                    if (configuredNum === finalManagerNumber) {
                                        driverIndex = mgr.index;
                                        console.log(`Found driver at Pmon index ${driverIndex}`);
                                        break;
                                    }
                                }
                            }
                            if (driverIndex !== null) {
                                // Stop the driver first
                                console.log(`🔧 Stopping OPC UA driver at index ${driverIndex}...`);
                                const stopResult = await pmonClient.stopManager(driverIndex);
                                if (stopResult.success) {
                                    console.log(`✓ Stopped OPC UA driver`);
                                }
                                else {
                                    console.warn(`⚠️  Failed to stop driver: ${stopResult.error}`);
                                }
                                // Wait a moment for the driver to stop
                                await new Promise(resolve => setTimeout(resolve, 1000));
                                // Remove the driver from Pmon
                                console.log(`🔧 Removing OPC UA driver from Pmon...`);
                                const removeResult = await pmonClient.removeManager(driverIndex);
                                if (removeResult.success) {
                                    console.log(`✅ Successfully removed OPC UA driver from Pmon`);
                                    driverRemovedSuccessfully = true;
                                }
                                else {
                                    console.warn(`⚠️  Failed to remove driver from Pmon: ${removeResult.error}`);
                                }
                                // Delete the _OPCUA{num} datapoint
                                console.log(`🔧 Deleting manager datapoint ${managerDp}...`);
                                try {
                                    const deletedMgr = await this.winccoa.dpDelete(managerDp);
                                    if (deletedMgr) {
                                        console.log(`✅ Successfully deleted manager datapoint ${managerDp}`);
                                    }
                                    else {
                                        console.warn(`⚠️  Failed to delete manager datapoint ${managerDp}`);
                                    }
                                }
                                catch (deleteMgrError) {
                                    const deleteMgrErrorMsg = deleteMgrError instanceof Error ? deleteMgrError.message : String(deleteMgrError);
                                    console.warn(`⚠️  Error deleting manager datapoint: ${deleteMgrErrorMsg}`);
                                }
                                // Delete the _Driver{num} datapoint
                                // IMPORTANT: Never delete _Driver1, _Driver2, _Driver3 - these are reserved for system managers
                                if (finalManagerNumber > 3) {
                                    const driverDp = `_Driver${finalManagerNumber}`;
                                    console.log(`🔧 Deleting driver common datapoint ${driverDp}...`);
                                    try {
                                        if (this.checkDpExists(driverDp)) {
                                            const deletedDriver = await this.winccoa.dpDelete(driverDp);
                                            if (deletedDriver) {
                                                console.log(`✅ Successfully deleted driver common datapoint ${driverDp}`);
                                            }
                                            else {
                                                console.warn(`⚠️  Failed to delete driver common datapoint ${driverDp}`);
                                            }
                                        }
                                        else {
                                            console.log(`  Driver common datapoint ${driverDp} does not exist, skipping`);
                                        }
                                    }
                                    catch (deleteDriverError) {
                                        const deleteDriverErrorMsg = deleteDriverError instanceof Error ? deleteDriverError.message : String(deleteDriverError);
                                        console.warn(`⚠️  Error deleting driver common datapoint: ${deleteDriverErrorMsg}`);
                                    }
                                }
                                else {
                                    console.log(`Skipping _Driver${finalManagerNumber} deletion - reserved for system managers (1-3)`);
                                }
                            }
                            else {
                                console.warn(`⚠️  Could not find driver in Pmon (may already be removed)`);
                            }
                        }
                        catch (pmonError) {
                            const pmonErrorMsg = pmonError instanceof Error ? pmonError.message : String(pmonError);
                            console.warn(`⚠️  Could not remove driver from Pmon: ${pmonErrorMsg}`);
                        }
                    }
                    else {
                        console.log(`✓ Driver has ${remainingServers.length} remaining connection(s), keeping it active`);
                    }
                }
                catch (checkError) {
                    console.warn(`⚠️  Could not check for remaining connections:`, checkError);
                    // Continue anyway
                }
            }
            else {
                console.log(`No specific manager to check, proceeding with general cleanup...`);
            }
            // 4. Clean up any other unused _OPCUA{num} datapoints (client managers only, not server)
            console.log(`🔍 Checking for other unused OPC UA client manager datapoints and drivers...`);
            try {
                const allOpcuaDps = this.winccoa.dpNames('_OPCUA*', '_OPCUA');
                let cleanedCount = 0;
                let cleanedDrivers = 0;
                // Get Pmon information once for all cleanup operations
                const pmonClient = new PmonClient();
                let status;
                let managerList;
                try {
                    status = await pmonClient.getManagerStatus();
                    managerList = await pmonClient.getManagerList();
                }
                catch (pmonError) {
                    console.warn(`⚠️  Could not connect to Pmon for driver cleanup:`, pmonError);
                    // Continue with datapoint cleanup only
                }
                for (const opcuaDp of allOpcuaDps) {
                    // Skip _OPCUAPvssServer - that's for OPC UA Server functionality, not client
                    if (opcuaDp === '_OPCUAPvssServer') {
                        continue;
                    }
                    // Skip the one we successfully removed in step 3
                    if (opcuaDp === managerDp && driverRemovedSuccessfully) {
                        console.log(`  Skipping ${opcuaDp} - already processed in step 3`);
                        continue;
                    }
                    try {
                        // Check if datapoint still exists (might have been deleted already)
                        if (!this.checkDpExists(opcuaDp)) {
                            console.log(`  Skipping ${opcuaDp} - already deleted`);
                            continue;
                        }
                        // Check if this datapoint has any servers configured
                        const serversRaw = await this.winccoa.dpGet(`${opcuaDp}.Config.Servers`);
                        const servers = Array.isArray(serversRaw) ? serversRaw : [];
                        if (servers.length === 0) {
                            console.log(`🔧 Found unused client manager datapoint ${opcuaDp}, cleaning up...`);
                            // Extract manager number from datapoint name (e.g., _OPCUA4 -> 4)
                            const managerNumMatch = opcuaDp.match(/_OPCUA(\d+)/);
                            const managerNum = managerNumMatch && managerNumMatch[1] ? parseInt(managerNumMatch[1], 10) : null;
                            // Try to find and remove the corresponding driver from Pmon
                            if (managerNum !== null && status && managerList) {
                                let foundDriverIndex = null;
                                for (let i = 0; i < status.managers.length; i++) {
                                    const mgr = status.managers[i];
                                    if (!mgr)
                                        continue;
                                    const mgrDetails = managerList[mgr.index];
                                    if (!mgrDetails)
                                        continue;
                                    // Check if this is the OPC UA driver with the correct number
                                    const isOpcUaDriver = mgrDetails.manager?.toLowerCase().includes('opcua') ||
                                        mgrDetails.manager?.toLowerCase().includes('opc-ua');
                                    if (isOpcUaDriver) {
                                        // IMPORTANT: Drivers without -num parameter implicitly run as -num 1
                                        const numMatch = mgrDetails.commandlineOptions?.match(/-num\s+(\d+)/);
                                        const configuredNum = numMatch && numMatch[1] ? parseInt(numMatch[1], 10) : 1; // Default to 1 if no -num specified
                                        if (configuredNum === managerNum) {
                                            foundDriverIndex = mgr.index;
                                            break;
                                        }
                                    }
                                }
                                if (foundDriverIndex !== null) {
                                    console.log(`  🔧 Stopping driver at Pmon index ${foundDriverIndex}...`);
                                    try {
                                        await pmonClient.stopManager(foundDriverIndex);
                                        await new Promise(resolve => setTimeout(resolve, 500));
                                        console.log(`  🔧 Removing driver from Pmon...`);
                                        const removeResult = await pmonClient.removeManager(foundDriverIndex);
                                        if (removeResult.success) {
                                            console.log(`  ✅ Removed driver for ${opcuaDp}`);
                                            cleanedDrivers++;
                                        }
                                    }
                                    catch (removeDriverError) {
                                        console.warn(`  ⚠️  Could not remove driver for ${opcuaDp}:`, removeDriverError);
                                    }
                                }
                            }
                            // Delete the datapoint
                            const deleted = await this.winccoa.dpDelete(opcuaDp);
                            if (deleted) {
                                console.log(`  ✅ Deleted unused client manager datapoint ${opcuaDp}`);
                                cleanedCount++;
                            }
                            // Also delete the _Driver{num} datapoint if it exists
                            // IMPORTANT: Never delete _Driver1, _Driver2, _Driver3 - these are reserved for system managers
                            if (managerNum !== null && managerNum > 3) {
                                const driverDp = `_Driver${managerNum}`;
                                if (this.checkDpExists(driverDp)) {
                                    console.log(`  🔧 Deleting driver common datapoint ${driverDp}...`);
                                    const deletedDriver = await this.winccoa.dpDelete(driverDp);
                                    if (deletedDriver) {
                                        console.log(`  ✅ Deleted driver common datapoint ${driverDp}`);
                                        cleanedCount++;
                                    }
                                }
                            }
                            else if (managerNum !== null && managerNum <= 3) {
                                console.log(`  Skipping _Driver${managerNum} - reserved for system managers`);
                            }
                        }
                    }
                    catch (checkDpError) {
                        // Datapoint might not exist anymore or have issues, skip it
                        continue;
                    }
                }
                if (cleanedCount > 0 || cleanedDrivers > 0) {
                    console.log(`✅ Cleaned up ${cleanedCount} unused client manager datapoint(s) and ${cleanedDrivers} driver(s)`);
                }
                else {
                    console.log(`✓ No unused client manager datapoints or drivers found`);
                }
            }
            catch (cleanupError) {
                console.warn(`⚠️  Could not check for unused client manager datapoints:`, cleanupError);
                // Continue anyway
            }
            console.log('========================================');
            console.log('✓ OPC UA Connection Deletion Complete');
            if (connectionExists) {
                console.log(`  Connection: ${normalizedConnection}`);
                if (managerDp) {
                    console.log(`  Manager: ${managerDp}`);
                }
            }
            else {
                console.log(`  Connection did not exist, performed cleanup only`);
            }
            console.log('========================================');
            return true;
        }
        catch (error) {
            console.error('========================================');
            console.error('✗ OPC UA Connection Deletion Failed');
            console.error('========================================');
            console.error(`Error: ${error}`);
            throw error;
        }
    }
}
// Default export
export default OpcUaConnection;
//# sourceMappingURL=OpcUaConnection.js.map