import * as dgram from 'dgram';
import * as snmp from 'net-snmp';
import type { IOidSet, ISnmpConfig, TUpsModel, IUpsStatus } from './types.js';
import { UpsOidSets } from './oid-sets.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
  // Debug mode flag
  private debug: boolean = false;

  // 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
  };

  /**
   * 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;
  }
  
  /**
   * Enable debug mode
   */
  public enableDebug(): void {
    this.debug = true;
    console.log('SNMP debug mode enabled - detailed logs will be shown');
  }

  /**
   * 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}`);
    }
  }

  /**
   * Send an SNMP GET request using the net-snmp package
   * @param oid OID to query
   * @param config SNMP configuration
   * @param retryCount Current retry count (unused in this implementation)
   * @returns Promise resolving to the SNMP response value
   */
  public async snmpGet(
    oid: string, 
    config = this.DEFAULT_CONFIG, 
    retryCount = 0
  ): Promise<any> {
    return new Promise((resolve, reject) => {
      if (this.debug) {
        console.log(`Sending SNMP v${config.version} GET request for OID ${oid} to ${config.host}:${config.port}`);
        console.log('Using community:', config.community);
      }

      // Create SNMP options based on configuration
      const options: any = {
        port: config.port,
        retries: 2, // Number of retries
        timeout: config.timeout,
        transport: 'udp4',
        idBitsSize: 32,
        context: config.context || ''
      };

      // Set version based on config
      if (config.version === 1) {
        options.version = snmp.Version1;
      } else if (config.version === 2) {
        options.version = snmp.Version2c;
      } else {
        options.version = snmp.Version3;
      }

      // Create appropriate session based on SNMP version
      let session;
      
      if (config.version === 3) {
        // For SNMPv3, we need to set up authentication and privacy
        // For SNMPv3, we need a valid security level
        const securityLevel = config.securityLevel || 'noAuthNoPriv';
        
        // Create the user object with required structure for net-snmp
        const user: any = {
          name: config.username || ''
        };
        
        // Set security level
        if (securityLevel === 'noAuthNoPriv') {
          user.level = snmp.SecurityLevel.noAuthNoPriv;
        } else if (securityLevel === 'authNoPriv') {
          user.level = snmp.SecurityLevel.authNoPriv;
          
          // Set auth protocol - must provide both protocol and key
          if (config.authProtocol && config.authKey) {
            if (config.authProtocol === 'MD5') {
              user.authProtocol = snmp.AuthProtocols.md5;
            } else if (config.authProtocol === 'SHA') {
              user.authProtocol = snmp.AuthProtocols.sha;
            }
            user.authKey = config.authKey;
          } else {
            // Fallback to noAuthNoPriv if auth details missing
            user.level = snmp.SecurityLevel.noAuthNoPriv;
            if (this.debug) {
              console.log('Warning: Missing authProtocol or authKey, falling back to noAuthNoPriv');
            }
          }
        } else if (securityLevel === 'authPriv') {
          user.level = snmp.SecurityLevel.authPriv;
          
          // Set auth protocol - must provide both protocol and key
          if (config.authProtocol && config.authKey) {
            if (config.authProtocol === 'MD5') {
              user.authProtocol = snmp.AuthProtocols.md5;
            } else if (config.authProtocol === 'SHA') {
              user.authProtocol = snmp.AuthProtocols.sha;
            }
            user.authKey = config.authKey;
            
            // Set privacy protocol - must provide both protocol and key
            if (config.privProtocol && config.privKey) {
              if (config.privProtocol === 'DES') {
                user.privProtocol = snmp.PrivProtocols.des;
              } else if (config.privProtocol === 'AES') {
                user.privProtocol = snmp.PrivProtocols.aes;
              }
              user.privKey = config.privKey;
            } else {
              // Fallback to authNoPriv if priv details missing
              user.level = snmp.SecurityLevel.authNoPriv;
              if (this.debug) {
                console.log('Warning: Missing privProtocol or privKey, falling back to authNoPriv');
              }
            }
          } else {
            // Fallback to noAuthNoPriv if auth details missing
            user.level = snmp.SecurityLevel.noAuthNoPriv;
            if (this.debug) {
              console.log('Warning: Missing authProtocol or authKey, falling back to noAuthNoPriv');
            }
          }
        }
        
        if (this.debug) {
          console.log('SNMPv3 user configuration:', {
            name: user.name,
            level: Object.keys(snmp.SecurityLevel).find(key => snmp.SecurityLevel[key] === user.level),
            authProtocol: user.authProtocol ? 'Set' : 'Not Set',
            authKey: user.authKey ? 'Set' : 'Not Set',
            privProtocol: user.privProtocol ? 'Set' : 'Not Set',
            privKey: user.privKey ? 'Set' : 'Not Set'
          });
        }
        
        session = snmp.createV3Session(config.host, user, options);
      } else {
        // For SNMPv1/v2c, we use the community string
        session = snmp.createSession(config.host, config.community || 'public', options);
      }

      // Convert the OID string to an array of OIDs if multiple OIDs are needed
      const oids = [oid];

      // Send the GET request
      session.get(oids, (error: any, varbinds: any[]) => {
        // Close the session to release resources
        session.close();

        if (error) {
          if (this.debug) {
            console.error('SNMP GET error:', error);
          }
          reject(new Error(`SNMP GET error: ${error.message || error}`));
          return;
        }

        if (!varbinds || varbinds.length === 0) {
          if (this.debug) {
            console.error('No varbinds returned in response');
          }
          reject(new Error('No varbinds returned in response'));
          return;
        }

        // Check for SNMP errors in the response
        if (varbinds[0].type === snmp.ObjectType.NoSuchObject ||
            varbinds[0].type === snmp.ObjectType.NoSuchInstance ||
            varbinds[0].type === snmp.ObjectType.EndOfMibView) {
          if (this.debug) {
            console.error('SNMP error:', snmp.ObjectType[varbinds[0].type]);
          }
          reject(new Error(`SNMP error: ${snmp.ObjectType[varbinds[0].type]}`));
          return;
        }

        // Process the response value based on its type
        let value = varbinds[0].value;

        // Handle specific types that might need conversion
        if (Buffer.isBuffer(value)) {
          // If value is a Buffer, try to convert it to a string if it's printable ASCII
          const isPrintableAscii = value.every(byte => byte >= 32 && byte <= 126);
          if (isPrintableAscii) {
            value = value.toString();
          }
        } else if (typeof value === 'bigint') {
          // Convert BigInt to a normal number or string if needed
          value = Number(value);
        }

        if (this.debug) {
          console.log('SNMP response:', {
            oid: varbinds[0].oid,
            type: varbinds[0].type,
            value: value
          });
        }

        resolve(value);
      });
    });
  }

  /**
   * 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('---------------------------------------');
      }
      
      // Get all values with independent retry logic
      const powerStatusValue = await this.getSNMPValueWithRetry(this.activeOIDs.POWER_STATUS, 'power status', config);
      const batteryCapacity = await this.getSNMPValueWithRetry(this.activeOIDs.BATTERY_CAPACITY, 'battery capacity', config) || 0;
      const batteryRuntime = await this.getSNMPValueWithRetry(this.activeOIDs.BATTERY_RUNTIME, 'battery runtime', config) || 0;
      
      // Determine power status - handle different values for different UPS models
      const powerStatus = this.determinePowerStatus(config.upsModel, powerStatusValue);
      
      // Convert to minutes for UPS models with different time units
      const processedRuntime = this.processRuntimeValue(config.upsModel, batteryRuntime);
      
      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}`);
    }
  }

  /**
   * Helper method to get SNMP value with retry and fallback logic
   * @param oid OID to query
   * @param description Description of the value for logging
   * @param config SNMP configuration
   * @returns Promise resolving to the SNMP value
   */
  private async getSNMPValueWithRetry(
    oid: string, 
    description: string, 
    config: ISnmpConfig
  ): Promise<any> {
    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're using SNMPv3, try with different security levels
      if (config.version === 3) {
        return await this.tryFallbackSecurityLevels(oid, description, config);
      }
      
      // Try with standard OIDs as fallback
      if (config.upsModel !== 'custom') {
        return await this.tryStandardOids(oid, description, config);
      }
      
      // Return a default value if all attempts fail
      if (this.debug) {
        console.log(`Using default value 0 for ${description}`);
      }
      return 0;
    }
  }

  /**
   * Try fallback security levels for SNMPv3
   * @param oid OID to query
   * @param description Description of the value for logging
   * @param config SNMP configuration
   * @returns Promise resolving to the SNMP value
   */
  private async tryFallbackSecurityLevels(
    oid: string, 
    description: string, 
    config: ISnmpConfig
  ): Promise<any> {
    if (this.debug) {
      console.log(`Retrying ${description} with fallback security level...`);
    }
    
    // Try with authNoPriv if current level is authPriv
    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);
        }
      }
    }
    
    // Try with noAuthNoPriv as a last resort
    if (config.securityLevel === 'authPriv' || config.securityLevel === 'authNoPriv') {
      const retryConfig = { ...config, securityLevel: 'noAuthNoPriv' as 'noAuthNoPriv' };
      try {
        if (this.debug) {
          console.log(`Retrying with noAuthNoPriv 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);
        }
      }
    }
    
    return 0;
  }

  /**
   * Try standard OIDs as fallback
   * @param oid OID to query
   * @param description Description of the value for logging
   * @param config SNMP configuration
   * @returns Promise resolving to the SNMP value
   */
  private async tryStandardOids(
    oid: string, 
    description: string, 
    config: ISnmpConfig
  ): Promise<any> {
    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 0;
  }

  /**
   * Determine power status based on UPS model and raw value
   * @param upsModel UPS model
   * @param powerStatusValue Raw power status value
   * @returns Standardized power status
   */
  private determinePowerStatus(
    upsModel: TUpsModel | undefined, 
    powerStatusValue: number
  ): 'online' | 'onBattery' | 'unknown' {
    if (upsModel === 'cyberpower') {
      // CyberPower RMCARD205: upsBaseOutputStatus values
      // 2=onLine, 3=onBattery, 4=onBoost, 5=onSleep, 6=off, etc.
      if (powerStatusValue === 2) {
        return 'online';
      } else if (powerStatusValue === 3) {
        return 'onBattery';
      }
    } else if (upsModel === 'eaton') {
      // Eaton UPS: xupsOutputSource values
      // 3=normal/mains, 5=battery, etc.
      if (powerStatusValue === 3) {
        return 'online';
      } else if (powerStatusValue === 5) {
        return 'onBattery';
      }
    } else if (upsModel === 'apc') {
      // APC UPS: upsBasicOutputStatus values
      // 2=online, 3=onBattery, etc.
      if (powerStatusValue === 2) {
        return 'online';
      } else if (powerStatusValue === 3) {
        return 'onBattery';
      }
    } else {
      // Default interpretation for other UPS models
      if (powerStatusValue === 1) {
        return 'online';
      } else if (powerStatusValue === 2) {
        return 'onBattery';
      }
    }
    
    return 'unknown';
  }

  /**
   * Process runtime value based on UPS model
   * @param upsModel UPS model
   * @param batteryRuntime Raw battery runtime value
   * @returns Processed runtime in minutes
   */
  private processRuntimeValue(
    upsModel: TUpsModel | undefined, 
    batteryRuntime: number
  ): number {
    if (this.debug) {
      console.log('Raw runtime value:', batteryRuntime);
    }
  
    if (upsModel === 'cyberpower' && batteryRuntime > 0) {
      // CyberPower: TimeTicks is in 1/100 seconds, convert to minutes
      const minutes = Math.floor(batteryRuntime / 6000); // 6000 ticks = 1 minute
      if (this.debug) {
        console.log(`Converting CyberPower runtime from ${batteryRuntime} ticks to ${minutes} minutes`);
      }
      return minutes;
    } else if (upsModel === 'eaton' && batteryRuntime > 0) {
      // Eaton: Runtime is in seconds, convert to minutes
      const minutes = Math.floor(batteryRuntime / 60);
      if (this.debug) {
        console.log(`Converting Eaton runtime from ${batteryRuntime} seconds to ${minutes} minutes`);
      }
      return minutes;
    } else if (batteryRuntime > 10000) {
      // Generic conversion for large tick values (likely TimeTicks)
      const minutes = Math.floor(batteryRuntime / 6000);
      if (this.debug) {
        console.log(`Converting ${batteryRuntime} ticks to ${minutes} minutes`);
      }
      return minutes;
    }
    
    return batteryRuntime;
  }
}