import * as dgram from 'dgram';
import type { IOidSet, ISnmpConfig, TUpsModel, IUpsStatus } from './types.js';
import { UpsOidSets } from './oid-sets.js';
import { SnmpPacketCreator } from './packet-creator.js';
import { SnmpPacketParser } from './packet-parser.js';

/**
 * Class for SNMP communication with UPS devices
 * Main entry point for SNMP functionality
 */
export class NupstSnmp {
  // Active OID set
  private activeOIDs: IOidSet;
  // Reference to the parent Nupst instance
  private nupst: any; // Type 'any' to avoid circular dependency

  // Default SNMP configuration
  private readonly DEFAULT_CONFIG: ISnmpConfig = {
    host: '127.0.0.1', // Default to localhost
    port: 161, // Default SNMP port
    community: 'public', // Default community string for v1/v2c
    version: 1, // SNMPv1
    timeout: 5000, // 5 seconds timeout
    upsModel: 'cyberpower', // Default UPS model
  };

  // SNMPv3 engine ID and counters
  private engineID: Buffer = Buffer.from([0x80, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06]);
  private engineBoots: number = 0;
  private engineTime: number = 0;
  private requestID: number = 1;
  private debug: boolean = false; // Enable for debug output

  /**
   * Create a new SNMP manager
   * @param debug Whether to enable debug mode
   */
  constructor(debug = false) {
    this.debug = debug;
    // Set default OID set
    this.activeOIDs = UpsOidSets.getOidSet('cyberpower');
  }
  
  /**
   * Set reference to the main Nupst instance
   * @param nupst Reference to the main Nupst instance
   */
  public setNupst(nupst: any): void {
    this.nupst = nupst;
  }
  
  /**
   * Get reference to the main Nupst instance
   */
  public getNupst(): any {
    return this.nupst;
  }
  
  /**
   * Set active OID set based on UPS model
   * @param config SNMP configuration
   */
  private setActiveOIDs(config: ISnmpConfig): void {
    // If custom OIDs are provided, use them
    if (config.upsModel === 'custom' && config.customOIDs) {
      this.activeOIDs = config.customOIDs;
      if (this.debug) {
        console.log('Using custom OIDs:', this.activeOIDs);
      }
      return;
    }
    
    // Use OIDs for the specified UPS model or default to Cyberpower
    const model = config.upsModel || 'cyberpower';
    this.activeOIDs = UpsOidSets.getOidSet(model);
    
    if (this.debug) {
      console.log(`Using OIDs for UPS model: ${model}`);
    }
  }
  
  /**
   * Enable debug mode
   */
  public enableDebug(): void {
    this.debug = true;
    console.log('SNMP debug mode enabled - detailed logs will be shown');
  }

  /**
   * Send an SNMP GET request
   * @param oid OID to query
   * @param config SNMP configuration
   * @returns Promise resolving to the SNMP response value
   */
  public async snmpGet(oid: string, config = this.DEFAULT_CONFIG): Promise<any> {
    return new Promise((resolve, reject) => {
      const socket = dgram.createSocket('udp4');
      
      // Create appropriate request based on SNMP version
      let request: Buffer;
      if (config.version === 3) {
        request = SnmpPacketCreator.createSnmpV3GetRequest(
          oid, 
          config, 
          this.engineID, 
          this.engineBoots, 
          this.engineTime, 
          this.requestID++,
          this.debug
        );
      } else {
        request = SnmpPacketCreator.createSnmpGetRequest(oid, config.community || 'public', this.debug);
      }
      
      if (this.debug) {
        console.log(`Sending SNMP ${config.version === 3 ? 'v3' : ('v' + config.version)} request to ${config.host}:${config.port}`);
        console.log('Request length:', request.length);
        console.log('First 16 bytes of request:', request.slice(0, 16).toString('hex'));
        console.log('Full request hex:', request.toString('hex'));
      }
      
      // Set timeout - add extra logging for debugging
      const timeout = setTimeout(() => {
        socket.close();
        if (this.debug) {
          console.error('---------------------------------------');
          console.error('SNMP request timed out after', config.timeout, 'ms');
          console.error('SNMP Version:', config.version);
          if (config.version === 3) {
            console.error('SNMPv3 Security Level:', config.securityLevel);
            console.error('SNMPv3 Username:', config.username);
            console.error('SNMPv3 Auth Protocol:', config.authProtocol || 'None');
            console.error('SNMPv3 Privacy Protocol:', config.privProtocol || 'None');
          }
          console.error('OID:', oid);
          console.error('Host:', config.host);
          console.error('Port:', config.port);
          console.error('---------------------------------------');
        }
        reject(new Error(`SNMP request timed out after ${config.timeout}ms`));
      }, config.timeout);
      
      // Listen for responses
      socket.on('message', (message, rinfo) => {
        clearTimeout(timeout);
        
        if (this.debug) {
          console.log(`Received SNMP response from ${rinfo.address}:${rinfo.port}`);
          console.log('Response length:', message.length);
          console.log('First 16 bytes of response:', message.slice(0, 16).toString('hex'));
          console.log('Full response hex:', message.toString('hex'));
        }
        
        try {
          const result = SnmpPacketParser.parseSnmpResponse(message, config, this.debug);
          
          if (this.debug) {
            console.log('Parsed SNMP response:', result);
          }
          
          socket.close();
          resolve(result);
        } catch (error) {
          if (this.debug) {
            console.error('Error parsing SNMP response:', error);
          }
          socket.close();
          reject(error);
        }
      });
      
      // Handle errors
      socket.on('error', (error) => {
        clearTimeout(timeout);
        socket.close();
        if (this.debug) {
          console.error('Socket error during SNMP request:', error);
        }
        reject(error);
      });
      
      // First send the request directly without binding to a specific port
      // This lets the OS pick an available port instead of trying to bind to one
      socket.send(request, 0, request.length, config.port, config.host, (error) => {
        if (error) {
          clearTimeout(timeout);
          socket.close();
          if (this.debug) {
            console.error('Error sending SNMP request:', error);
          }
          reject(error);
        } else if (this.debug) {
          console.log('SNMP request sent successfully');
        }
      });
    });
  }

  /**
   * Get the current status of the UPS
   * @param config SNMP configuration
   * @returns Promise resolving to the UPS status
   */
  public async getUpsStatus(config = this.DEFAULT_CONFIG): Promise<IUpsStatus> {
    try {
      // Set active OID set based on UPS model in config
      this.setActiveOIDs(config);
      
      if (this.debug) {
        console.log('---------------------------------------');
        console.log('Getting UPS status with config:');
        console.log('  Host:', config.host);
        console.log('  Port:', config.port);
        console.log('  Version:', config.version);
        console.log('  Timeout:', config.timeout, 'ms');
        console.log('  UPS Model:', config.upsModel || 'cyberpower');
        if (config.version === 1 || config.version === 2) {
          console.log('  Community:', config.community);
        } else if (config.version === 3) {
          console.log('  Security Level:', config.securityLevel);
          console.log('  Username:', config.username);
          console.log('  Auth Protocol:', config.authProtocol || 'None');
          console.log('  Privacy Protocol:', config.privProtocol || 'None');
        }
        console.log('Using OIDs:');
        console.log('  Power Status:', this.activeOIDs.POWER_STATUS);
        console.log('  Battery Capacity:', this.activeOIDs.BATTERY_CAPACITY);
        console.log('  Battery Runtime:', this.activeOIDs.BATTERY_RUNTIME);
        console.log('---------------------------------------');
      }
      
      // For SNMPv3, we need to discover the engine ID first
      if (config.version === 3) {
        if (this.debug) {
          console.log('SNMPv3 detected, starting engine ID discovery');
        }
        
        try {
          const discoveredEngineId = await this.discoverEngineId(config);
          if (discoveredEngineId) {
            this.engineID = discoveredEngineId;
            if (this.debug) {
              console.log('Using discovered engine ID:', this.engineID.toString('hex'));
            }
          }
        } catch (error) {
          if (this.debug) {
            console.warn('Engine ID discovery failed, using default:', error);
          }
        }
      }
      
      // Helper function to get SNMP value with retry
      const getSNMPValueWithRetry = async (oid: string, description: string) => {
        if (oid === '') {
          if (this.debug) {
            console.log(`No OID provided for ${description}, skipping`);
          }
          return 0;
        }
        
        if (this.debug) {
          console.log(`Getting ${description} OID: ${oid}`);
        }
        
        try {
          const value = await this.snmpGet(oid, config);
          if (this.debug) {
            console.log(`${description} value:`, value);
          }
          return value;
        } catch (error) {
          if (this.debug) {
            console.error(`Error getting ${description}:`, error.message);
          }
          
          // If we got a timeout and it's SNMPv3, try with different security levels
          if (error.message.includes('timed out') && config.version === 3) {
            if (this.debug) {
              console.log(`Retrying ${description} with fallback settings...`);
            }
            
            // Create a retry config with lower security level
            if (config.securityLevel === 'authPriv') {
              const retryConfig = { ...config, securityLevel: 'authNoPriv' as 'authNoPriv' };
              try {
                if (this.debug) {
                  console.log(`Retrying with authNoPriv security level`);
                }
                const value = await this.snmpGet(oid, retryConfig);
                if (this.debug) {
                  console.log(`${description} retry value:`, value);
                }
                return value;
              } catch (retryError) {
                if (this.debug) {
                  console.error(`Retry failed for ${description}:`, retryError.message);
                }
              }
            }
          }
          
          // If we're still having trouble, try with standard OIDs
          if (config.upsModel !== 'custom') {
            try {
              // Try RFC 1628 standard UPS MIB OIDs
              const standardOIDs = UpsOidSets.getStandardOids();
              
              if (this.debug) {
                console.log(`Trying standard RFC 1628 OID for ${description}: ${standardOIDs[description]}`);
              }
              
              const standardValue = await this.snmpGet(standardOIDs[description], config);
              if (this.debug) {
                console.log(`${description} standard OID value:`, standardValue);
              }
              return standardValue;
            } catch (stdError) {
              if (this.debug) {
                console.error(`Standard OID retry failed for ${description}:`, stdError.message);
              }
            }
          }
          
          // Return a default value if all attempts fail
          if (this.debug) {
            console.log(`Using default value 0 for ${description}`);
          }
          return 0;
        }
      };
      
      // Get all values with independent retry logic
      const powerStatusValue = await getSNMPValueWithRetry(this.activeOIDs.POWER_STATUS, 'power status');
      const batteryCapacity = await getSNMPValueWithRetry(this.activeOIDs.BATTERY_CAPACITY, 'battery capacity') || 0;
      const batteryRuntime = await getSNMPValueWithRetry(this.activeOIDs.BATTERY_RUNTIME, 'battery runtime') || 0;
      
      // Determine power status - handle different values for different UPS models
      let powerStatus: 'online' | 'onBattery' | 'unknown' = 'unknown';
      
      // Different UPS models use different values for power status
      if (config.upsModel === 'cyberpower') {
        // CyberPower RMCARD205: upsBaseOutputStatus values
        // 2=onLine, 3=onBattery, 4=onBoost, 5=onSleep, 6=off, etc.
        if (powerStatusValue === 2) {
          powerStatus = 'online';
        } else if (powerStatusValue === 3) {
          powerStatus = 'onBattery';
        }
      } else {
        // Default interpretation for other UPS models
        if (powerStatusValue === 1) {
          powerStatus = 'online';
        } else if (powerStatusValue === 2) {
          powerStatus = 'onBattery';
        }
      }
      
      // Convert TimeTicks to minutes for CyberPower runtime (value is in 1/100 seconds)
      let processedRuntime = batteryRuntime;
      if (config.upsModel === 'cyberpower' && batteryRuntime > 0) {
        // TimeTicks is in 1/100 seconds, convert to minutes
        processedRuntime = Math.floor(batteryRuntime / 6000); // 6000 ticks = 1 minute
        if (this.debug) {
          console.log(`Converting CyberPower runtime from ${batteryRuntime} ticks to ${processedRuntime} minutes`);
        }
      }
      
      const result = {
        powerStatus,
        batteryCapacity,
        batteryRuntime: processedRuntime,
        raw: {
          powerStatus: powerStatusValue,
          batteryCapacity,
          batteryRuntime,
        },
      };
      
      if (this.debug) {
        console.log('---------------------------------------');
        console.log('UPS status result:');
        console.log('  Power Status:', result.powerStatus);
        console.log('  Battery Capacity:', result.batteryCapacity + '%');
        console.log('  Battery Runtime:', result.batteryRuntime, 'minutes');
        console.log('---------------------------------------');
      }
      
      return result;
    } catch (error) {
      if (this.debug) {
        console.error('---------------------------------------');
        console.error('Error getting UPS status:', error.message);
        console.error('---------------------------------------');
      }
      throw new Error(`Failed to get UPS status: ${error.message}`);
    }
  }

  /**
   * Discover SNMP engine ID (for SNMPv3)
   * Sends a proper discovery message to get the engine ID from the device
   * @param config SNMP configuration
   * @returns Promise resolving to the discovered engine ID
   */
  public async discoverEngineId(config: ISnmpConfig): Promise<Buffer> {
    return new Promise((resolve, reject) => {
      const socket = dgram.createSocket('udp4');
      
      // Create a proper discovery message (SNMPv3 with noAuthNoPriv)
      const discoveryConfig: ISnmpConfig = {
        ...config,
        securityLevel: 'noAuthNoPriv',
        username: '',  // Empty username for discovery
      };
      
      // Create a simple GetRequest for sysDescr (a commonly available OID)
      const request = SnmpPacketCreator.createDiscoveryMessage(discoveryConfig, this.requestID++);
      
      if (this.debug) {
        console.log('Sending SNMPv3 discovery message');
        console.log('SNMPv3 Discovery message:', request.toString('hex'));
      }
      
      // Set timeout - use a longer timeout for discovery phase
      const discoveryTimeout = Math.max(config.timeout, 15000); // At least 15 seconds for discovery
      const timeout = setTimeout(() => {
        socket.close();
        // Fall back to default engine ID if discovery fails
        if (this.debug) {
          console.error('---------------------------------------');
          console.error('Engine ID discovery timed out after', discoveryTimeout, 'ms');
          console.error('SNMPv3 settings:');
          console.error('  Username:', config.username);
          console.error('  Security Level:', config.securityLevel);
          console.error('  Host:', config.host);
          console.error('  Port:', config.port);
          console.error('Using default engine ID:', this.engineID.toString('hex'));
          console.error('---------------------------------------');
        }
        resolve(this.engineID);
      }, discoveryTimeout);
      
      // Listen for responses
      socket.on('message', (message, rinfo) => {
        clearTimeout(timeout);
        
        if (this.debug) {
          console.log(`Received SNMPv3 discovery response from ${rinfo.address}:${rinfo.port}`);
          console.log('Response:', message.toString('hex'));
        }
        
        try {
          // Extract engine ID from response
          const engineId = SnmpPacketParser.extractEngineId(message, this.debug);
          if (engineId) {
            this.engineID = engineId; // Update the engine ID
            if (this.debug) {
              console.log('Discovered engine ID:', engineId.toString('hex'));
            }
            socket.close();
            resolve(engineId);
          } else {
            if (this.debug) {
              console.log('Could not extract engine ID, using default');
            }
            socket.close();
            resolve(this.engineID);
          }
        } catch (error) {
          if (this.debug) {
            console.error('Error extracting engine ID:', error);
          }
          socket.close();
          resolve(this.engineID); // Fall back to default engine ID
        }
      });
      
      // Handle errors
      socket.on('error', (error) => {
        clearTimeout(timeout);
        socket.close();
        if (this.debug) {
          console.error('Engine ID discovery socket error:', error);
        }
        resolve(this.engineID); // Fall back to default engine ID
      });
      
      // Send request directly without binding
      socket.send(request, 0, request.length, config.port, config.host, (error) => {
        if (error) {
          clearTimeout(timeout);
          socket.close();
          if (this.debug) {
            console.error('Error sending discovery message:', error);
          }
          resolve(this.engineID); // Fall back to default engine ID
        } else if (this.debug) {
          console.log('Discovery message sent successfully');
        }
      });
    });
  }

  // initiateShutdown method has been moved to the NupstDaemon class
}