import * as crypto from 'crypto';
import type { ISnmpConfig, ISnmpV3SecurityParams } from './types.js';
import { SnmpEncoder } from './encoder.js';

/**
 * SNMP packet creation utilities
 * Creates SNMP request packets for different SNMP versions
 */
export class SnmpPacketCreator {
  /**
   * Create an SNMPv1 GET request
   * @param oid OID to query
   * @param community Community string
   * @param debug Whether to enable debug output
   * @returns Buffer containing the SNMP request
   */
  public static createSnmpGetRequest(oid: string, community: string, debug: boolean = false): Buffer {
    const oidArray = SnmpEncoder.oidToArray(oid);
    const encodedOid = SnmpEncoder.encodeOID(oidArray);
    
    if (debug) {
      console.log('OID array length:', oidArray.length);
      console.log('OID array:', oidArray);
    }
    
    // SNMP message structure
    // Sequence
    //   Version (Integer)
    //   Community (String)
    //   PDU (GetRequest)
    //     Request ID (Integer)
    //     Error Status (Integer)
    //     Error Index (Integer)
    //     Variable Bindings (Sequence)
    //       Variable (Sequence)
    //         OID (ObjectIdentifier)
    //         Value (Null)
    
    // Use the standard method from our test that is known to work
    // Create a fixed request ID (0x00000001) to ensure deterministic behavior
    const requestId = Buffer.from([0x00, 0x00, 0x00, 0x01]);
    
    // Encode values
    const versionBuf = Buffer.concat([
      Buffer.from([0x02, 0x01]), // ASN.1 Integer, length 1
      Buffer.from([0x00])        // SNMP version 1 (0)
    ]);
    
    const communityBuf = Buffer.concat([
      Buffer.from([0x04, community.length]), // ASN.1 Octet String, length
      Buffer.from(community)                // Community string
    ]);
    
    const requestIdBuf = Buffer.concat([
      Buffer.from([0x02, 0x04]), // ASN.1 Integer, length 4
      requestId                  // Fixed Request ID
    ]);
    
    const errorStatusBuf = Buffer.concat([
      Buffer.from([0x02, 0x01]), // ASN.1 Integer, length 1
      Buffer.from([0x00])        // Error Status (0 = no error)
    ]);
    
    const errorIndexBuf = Buffer.concat([
      Buffer.from([0x02, 0x01]), // ASN.1 Integer, length 1
      Buffer.from([0x00])        // Error Index (0)
    ]);
    
    const oidValueBuf = Buffer.concat([
      Buffer.from([0x30]),       // ASN.1 Sequence
      Buffer.from([encodedOid.length + 2]), // Length
      Buffer.from([0x06]),       // ASN.1 Object Identifier
      Buffer.from([encodedOid.length]), // Length
      encodedOid,                // OID
      Buffer.from([0x05, 0x00])  // Null value
    ]);
    
    const varBindingsBuf = Buffer.concat([
      Buffer.from([0x30]),       // ASN.1 Sequence
      Buffer.from([oidValueBuf.length]), // Length
      oidValueBuf                // Variable binding
    ]);
    
    const pduBuf = Buffer.concat([
      Buffer.from([0xa0]),       // ASN.1 Context-specific Constructed 0 (GetRequest)
      Buffer.from([requestIdBuf.length + errorStatusBuf.length + errorIndexBuf.length + varBindingsBuf.length]), // Length
      requestIdBuf,              // Request ID
      errorStatusBuf,            // Error Status
      errorIndexBuf,             // Error Index
      varBindingsBuf             // Variable Bindings
    ]);
    
    const messageBuf = Buffer.concat([
      Buffer.from([0x30]),       // ASN.1 Sequence
      Buffer.from([versionBuf.length + communityBuf.length + pduBuf.length]), // Length
      versionBuf,                // Version
      communityBuf,              // Community
      pduBuf                     // PDU
    ]);
    
    if (debug) {
      console.log('SNMP Request buffer:', messageBuf.toString('hex'));
    }
    
    return messageBuf;
  }

  /**
   * Create an SNMPv3 GET request
   * @param oid OID to query
   * @param config SNMP configuration
   * @param engineID Engine ID
   * @param engineBoots Engine boots counter
   * @param engineTime Engine time counter
   * @param requestID Request ID
   * @param debug Whether to enable debug output
   * @returns Buffer containing the SNMP request
   */
  public static createSnmpV3GetRequest(
    oid: string, 
    config: ISnmpConfig, 
    engineID: Buffer,
    engineBoots: number,
    engineTime: number,
    requestID: number,
    debug: boolean = false
  ): Buffer {
    if (debug) {
      console.log('Creating SNMPv3 GET request for OID:', oid);
      console.log('With config:', {
        ...config,
        authKey: config.authKey ? '***' : undefined,
        privKey: config.privKey ? '***' : undefined
      });
    }
    
    const oidArray = SnmpEncoder.oidToArray(oid);
    const encodedOid = SnmpEncoder.encodeOID(oidArray);
    
    if (debug) {
      console.log('Using engine ID:', engineID.toString('hex'));
      console.log('Engine boots:', engineBoots);
      console.log('Engine time:', engineTime);
      console.log('Request ID:', requestID);
    }
    
    // Create security parameters
    const securityParams: ISnmpV3SecurityParams = {
      msgAuthoritativeEngineID: engineID,
      msgAuthoritativeEngineBoots: engineBoots,
      msgAuthoritativeEngineTime: engineTime,
      msgUserName: config.username || '',
      msgAuthenticationParameters: Buffer.alloc(12, 0), // Will be filled in later for auth
      msgPrivacyParameters: Buffer.alloc(8, 0),  // For privacy
    };

    // Create the PDU (Protocol Data Unit)
    // This is wrapped within the security parameters
    const requestIdBuf = Buffer.concat([
      Buffer.from([0x02, 0x04]), // ASN.1 Integer, length 4
      SnmpEncoder.encodeInteger(requestID) // Request ID
    ]);
    
    const errorStatusBuf = Buffer.concat([
      Buffer.from([0x02, 0x01]), // ASN.1 Integer, length 1
      Buffer.from([0x00])        // Error Status (0 = no error)
    ]);
    
    const errorIndexBuf = Buffer.concat([
      Buffer.from([0x02, 0x01]), // ASN.1 Integer, length 1
      Buffer.from([0x00])        // Error Index (0)
    ]);
    
    const oidValueBuf = Buffer.concat([
      Buffer.from([0x30]),       // ASN.1 Sequence
      Buffer.from([encodedOid.length + 2]), // Length
      Buffer.from([0x06]),       // ASN.1 Object Identifier
      Buffer.from([encodedOid.length]), // Length
      encodedOid,                // OID
      Buffer.from([0x05, 0x00])  // Null value
    ]);
    
    const varBindingsBuf = Buffer.concat([
      Buffer.from([0x30]),       // ASN.1 Sequence
      Buffer.from([oidValueBuf.length]), // Length
      oidValueBuf                // Variable binding
    ]);
    
    const pduBuf = Buffer.concat([
      Buffer.from([0xa0]),       // ASN.1 Context-specific Constructed 0 (GetRequest)
      Buffer.from([requestIdBuf.length + errorStatusBuf.length + errorIndexBuf.length + varBindingsBuf.length]), // Length
      requestIdBuf,              // Request ID
      errorStatusBuf,            // Error Status
      errorIndexBuf,             // Error Index
      varBindingsBuf             // Variable Bindings
    ]);

    // Create the security parameters
    const engineIdBuf = Buffer.concat([
      Buffer.from([0x04, securityParams.msgAuthoritativeEngineID.length]), // ASN.1 Octet String
      securityParams.msgAuthoritativeEngineID
    ]);
    
    const engineBootsBuf = Buffer.concat([
      Buffer.from([0x02, 0x04]), // ASN.1 Integer, length 4
      SnmpEncoder.encodeInteger(securityParams.msgAuthoritativeEngineBoots)
    ]);
    
    const engineTimeBuf = Buffer.concat([
      Buffer.from([0x02, 0x04]), // ASN.1 Integer, length 4
      SnmpEncoder.encodeInteger(securityParams.msgAuthoritativeEngineTime)
    ]);
    
    const userNameBuf = Buffer.concat([
      Buffer.from([0x04, securityParams.msgUserName.length]), // ASN.1 Octet String
      Buffer.from(securityParams.msgUserName)
    ]);
    
    const authParamsBuf = Buffer.concat([
      Buffer.from([0x04, securityParams.msgAuthenticationParameters.length]), // ASN.1 Octet String
      securityParams.msgAuthenticationParameters
    ]);
    
    const privParamsBuf = Buffer.concat([
      Buffer.from([0x04, securityParams.msgPrivacyParameters.length]), // ASN.1 Octet String
      securityParams.msgPrivacyParameters
    ]);
    
    // Security parameters sequence
    const securityParamsBuf = Buffer.concat([
      Buffer.from([0x30]), // ASN.1 Sequence
      Buffer.from([engineIdBuf.length + engineBootsBuf.length + engineTimeBuf.length + 
                   userNameBuf.length + authParamsBuf.length + privParamsBuf.length]), // Length
      engineIdBuf,
      engineBootsBuf,
      engineTimeBuf,
      userNameBuf,
      authParamsBuf,
      privParamsBuf
    ]);

    // Determine security level flags
    let securityFlags = 0;
    if (config.securityLevel === 'authNoPriv' || config.securityLevel === 'authPriv') {
      securityFlags |= 0x01; // Authentication flag
    }
    if (config.securityLevel === 'authPriv') {
      securityFlags |= 0x02; // Privacy flag
    }
    
    // Set reportable flag - required for SNMPv3
    securityFlags |= 0x04; // Reportable flag

    // Create SNMPv3 header
    const msgIdBuf = Buffer.concat([
      Buffer.from([0x02, 0x04]), // ASN.1 Integer, length 4
      SnmpEncoder.encodeInteger(requestID) // Message ID (same as request ID for simplicity)
    ]);
    
    const msgMaxSizeBuf = Buffer.concat([
      Buffer.from([0x02, 0x04]), // ASN.1 Integer, length 4
      SnmpEncoder.encodeInteger(65507) // Max message size
    ]);
    
    const msgFlagsBuf = Buffer.concat([
      Buffer.from([0x04, 0x01]), // ASN.1 Octet String, length 1
      Buffer.from([securityFlags])
    ]);
    
    const msgSecModelBuf = Buffer.concat([
      Buffer.from([0x02, 0x01]), // ASN.1 Integer, length 1
      Buffer.from([0x03]) // Security model (3 = USM)
    ]);

    // SNMPv3 header
    const msgHeaderBuf = Buffer.concat([
      Buffer.from([0x30]), // ASN.1 Sequence
      Buffer.from([msgIdBuf.length + msgMaxSizeBuf.length + msgFlagsBuf.length + msgSecModelBuf.length]), // Length
      msgIdBuf,
      msgMaxSizeBuf,
      msgFlagsBuf,
      msgSecModelBuf
    ]);

    // SNMPv3 security parameters
    const msgSecurityBuf = Buffer.concat([
      Buffer.from([0x04]), // ASN.1 Octet String
      Buffer.from([securityParamsBuf.length]), // Length
      securityParamsBuf
    ]);

    // Create scopedPDU
    // In SNMPv3, the PDU is wrapped in a "scoped PDU" structure
    const contextEngineBuf = Buffer.concat([
      Buffer.from([0x04, engineID.length]), // ASN.1 Octet String
      engineID
    ]);
    
    const contextNameBuf = Buffer.concat([
      Buffer.from([0x04, 0x00]), // ASN.1 Octet String, length 0 (empty context name)
    ]);
    
    const scopedPduBuf = Buffer.concat([
      Buffer.from([0x30]), // ASN.1 Sequence
      Buffer.from([contextEngineBuf.length + contextNameBuf.length + pduBuf.length]), // Length
      contextEngineBuf,
      contextNameBuf,
      pduBuf
    ]);

    // For authPriv, we need to encrypt the scopedPDU
    let encryptedPdu = scopedPduBuf;
    if (config.securityLevel === 'authPriv' && config.privKey) {
      // In a real implementation, encryption would be applied here
      // For this example, we'll just simulate it
      encryptedPdu = this.simulateEncryption(scopedPduBuf, config);
    }

    // Final scopedPDU (encrypted or not)
    const finalScopedPduBuf = Buffer.concat([
      Buffer.from([0x04]), // ASN.1 Octet String
      Buffer.from([encryptedPdu.length]), // Length
      encryptedPdu
    ]);

    // Combine everything for the final message
    const versionBuf = Buffer.concat([
      Buffer.from([0x02, 0x01]), // ASN.1 Integer, length 1
      Buffer.from([0x03])        // SNMP version 3 (3)
    ]);
    
    const messageBuf = Buffer.concat([
      Buffer.from([0x30]), // ASN.1 Sequence
      Buffer.from([versionBuf.length + msgHeaderBuf.length + msgSecurityBuf.length + finalScopedPduBuf.length]), // Length
      versionBuf,
      msgHeaderBuf,
      msgSecurityBuf,
      finalScopedPduBuf
    ]);

    // If using authentication, calculate and insert the authentication parameters
    if ((config.securityLevel === 'authNoPriv' || config.securityLevel === 'authPriv') && 
        config.authKey && config.authProtocol) {
      const authenticatedMsg = this.addAuthentication(messageBuf, config, authParamsBuf);
      
      if (debug) {
        console.log('Created authenticated SNMPv3 message');
        console.log('Final message length:', authenticatedMsg.length);
      }
      
      return authenticatedMsg;
    }

    if (debug) {
      console.log('Created SNMPv3 message without authentication');
      console.log('Final message length:', messageBuf.length);
    }
    
    return messageBuf;
  }

  /**
   * Simulate encryption for authPriv security level
   * In a real implementation, this would use the specified privacy protocol (DES/AES)
   * @param data Data to encrypt
   * @param config SNMP configuration
   * @returns Encrypted data
   */
  private static simulateEncryption(data: Buffer, config: ISnmpConfig): Buffer {
    // This is a placeholder - in a real implementation, you would:
    // 1. Generate an initialization vector (IV)
    // 2. Use the privacy key derived from the privKey
    // 3. Apply the appropriate encryption algorithm (DES/AES)
    
    // For demonstration purposes only
    if (config.privProtocol === 'AES' && config.privKey) {
      try {
        // Create a deterministic IV for demo purposes (not secure for production)
        const iv = Buffer.alloc(16, 0);
        const engineID = Buffer.from([0x80, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06]);
        for (let i = 0; i < 8; i++) {
          iv[i] = engineID[i % engineID.length];
        }
        
        // Create a key from the privKey (proper key localization should be used in production)
        const key = crypto.createHash('md5').update(config.privKey).digest();
        
        // Create cipher and encrypt
        const cipher = crypto.createCipheriv('aes-128-cfb', key, iv);
        const encrypted = Buffer.concat([cipher.update(data), cipher.final()]);
        
        return encrypted;
      } catch (error) {
        console.warn('AES encryption failed, falling back to plaintext:', error);
        return data;
      }
    } else if (config.privProtocol === 'DES' && config.privKey) {
      try {
        // Create a deterministic IV for demo purposes (not secure for production)
        const iv = Buffer.alloc(8, 0);
        const engineID = Buffer.from([0x80, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06]);
        for (let i = 0; i < 8; i++) {
          iv[i] = engineID[i % engineID.length];
        }
        
        // Create a key from the privKey (proper key localization should be used in production)
        const key = crypto.createHash('md5').update(config.privKey).digest().slice(0, 8);
        
        // Create cipher and encrypt
        const cipher = crypto.createCipheriv('des-cbc', key, iv);
        const encrypted = Buffer.concat([cipher.update(data), cipher.final()]);
        
        return encrypted;
      } catch (error) {
        console.warn('DES encryption failed, falling back to plaintext:', error);
        return data;
      }
    }
    
    return data; // Return unencrypted data as fallback
  }

  /**
   * Add authentication to SNMPv3 message
   * @param message Message to authenticate
   * @param config SNMP configuration
   * @param authParamsBuf Authentication parameters buffer
   * @returns Authenticated message
   */
  private static addAuthentication(message: Buffer, config: ISnmpConfig, authParamsBuf: Buffer): Buffer {
    // In a real implementation, this would:
    // 1. Zero out the authentication parameters field
    // 2. Calculate HMAC-MD5 or HMAC-SHA1 over the entire message
    // 3. Insert the HMAC into the authentication parameters field
    
    if (!config.authKey) {
      return message;
    }
    
    try {
      // Find position of auth parameters in the message
      // This is a more reliable way to find the exact position
      let authParamsPos = -1;
      for (let i = 0; i < message.length - 16; i++) {
        // Look for the auth params pattern: 0x04 0x0C 0x00 0x00...
        if (message[i] === 0x04 && message[i + 1] === 0x0C) {
          // Check if next 12 bytes are all zeros
          let allZeros = true;
          for (let j = 0; j < 12; j++) {
            if (message[i + 2 + j] !== 0) {
              allZeros = false;
              break;
            }
          }
          if (allZeros) {
            authParamsPos = i;
            break;
          }
        }
      }
      
      if (authParamsPos === -1) {
        return message;
      }
      
      // Create a copy of the message with zeroed auth parameters
      const msgCopy = Buffer.from(message);
      
      // Prepare the authentication key according to RFC3414
      // We should use the standard key localization process
      const localizedKey = this.localizeAuthKey(config.authKey, 
        Buffer.from([0x80, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06]),
        config.authProtocol);
      
      // Calculate HMAC
      let hmac;
      if (config.authProtocol === 'SHA') {
        hmac = crypto.createHmac('sha1', localizedKey).update(msgCopy).digest().slice(0, 12);
      } else {
        // Default to MD5
        hmac = crypto.createHmac('md5', localizedKey).update(msgCopy).digest().slice(0, 12);
      }
      
      // Copy HMAC into original message
      hmac.copy(message, authParamsPos + 2);
      
      return message;
    } catch (error) {
      console.warn('Authentication failed:', error);
      return message;
    }
  }
  
  /**
   * Localize authentication key according to RFC3414
   * @param key Authentication key
   * @param engineId Engine ID
   * @param authProtocol Authentication protocol
   * @returns Localized key
   */
  private static localizeAuthKey(key: string, engineId: Buffer, authProtocol: string = 'MD5'): Buffer {
    try {
      // Convert password to key using hash
      let initialHash;
      if (authProtocol === 'SHA') {
        initialHash = crypto.createHash('sha1');
      } else {
        initialHash = crypto.createHash('md5');
      }
      
      // Generate the initial key - repeated hashing of password + padding
      const password = Buffer.from(key);
      let passwordIndex = 0;
      
      // Create a buffer of 1MB (1048576 bytes) filled with the password
      const buffer = Buffer.alloc(1048576);
      for (let i = 0; i < 1048576; i++) {
        buffer[i] = password[passwordIndex];
        passwordIndex = (passwordIndex + 1) % password.length;
      }
      
      initialHash.update(buffer);
      let initialKey = initialHash.digest();
      
      // Localize the key with engine ID
      let localHash;
      if (authProtocol === 'SHA') {
        localHash = crypto.createHash('sha1');
      } else {
        localHash = crypto.createHash('md5');
      }
      
      localHash.update(initialKey);
      localHash.update(engineId);
      localHash.update(initialKey);
      
      return localHash.digest();
    } catch (error) {
      console.error('Error localizing auth key:', error);
      // Return a fallback key
      return Buffer.from(key);
    }
  }

  /**
   * Create a discovery message for SNMPv3 engine ID discovery
   * @param config SNMP configuration
   * @param requestID Request ID
   * @returns Discovery message
   */
  public static createDiscoveryMessage(config: ISnmpConfig, requestID: number): Buffer {
    // Basic SNMPv3 header for discovery
    const msgIdBuf = Buffer.concat([
      Buffer.from([0x02, 0x04]), // ASN.1 Integer, length 4
      SnmpEncoder.encodeInteger(requestID)
    ]);
    
    const msgMaxSizeBuf = Buffer.concat([
      Buffer.from([0x02, 0x04]), // ASN.1 Integer, length 4
      SnmpEncoder.encodeInteger(65507) // Max message size
    ]);
    
    const msgFlagsBuf = Buffer.concat([
      Buffer.from([0x04, 0x01]), // ASN.1 Octet String, length 1
      Buffer.from([0x00]) // No authentication or privacy
    ]);
    
    const msgSecModelBuf = Buffer.concat([
      Buffer.from([0x02, 0x01]), // ASN.1 Integer, length 1
      Buffer.from([0x03]) // Security model (3 = USM)
    ]);

    // SNMPv3 header
    const msgHeaderBuf = Buffer.concat([
      Buffer.from([0x30]), // ASN.1 Sequence
      Buffer.from([msgIdBuf.length + msgMaxSizeBuf.length + msgFlagsBuf.length + msgSecModelBuf.length]), // Length
      msgIdBuf,
      msgMaxSizeBuf,
      msgFlagsBuf,
      msgSecModelBuf
    ]);
    
    // Simple security parameters for discovery
    const securityBuf = Buffer.concat([
      Buffer.from([0x04, 0x00]), // Empty octet string
    ]);
    
    // Simple Get request for discovery
    const requestIdBuf = Buffer.concat([
      Buffer.from([0x02, 0x04]), // ASN.1 Integer, length 4
      SnmpEncoder.encodeInteger(requestID + 1)
    ]);
    
    const errorStatusBuf = Buffer.concat([
      Buffer.from([0x02, 0x01]), // ASN.1 Integer, length 1
      Buffer.from([0x00])        // Error Status (0 = no error)
    ]);
    
    const errorIndexBuf = Buffer.concat([
      Buffer.from([0x02, 0x01]), // ASN.1 Integer, length 1
      Buffer.from([0x00])        // Error Index (0)
    ]);
    
    // Empty varbinds for discovery
    const varBindingsBuf = Buffer.concat([
      Buffer.from([0x30, 0x00]), // Empty sequence
    ]);
    
    const pduBuf = Buffer.concat([
      Buffer.from([0xa0]), // GetRequest
      Buffer.from([requestIdBuf.length + errorStatusBuf.length + errorIndexBuf.length + varBindingsBuf.length]),
      requestIdBuf,
      errorStatusBuf,
      errorIndexBuf,
      varBindingsBuf
    ]);
    
    // Context data
    const contextEngineBuf = Buffer.concat([
      Buffer.from([0x04, 0x00]), // Empty octet string
    ]);
    
    const contextNameBuf = Buffer.concat([
      Buffer.from([0x04, 0x00]), // Empty octet string
    ]);
    
    const scopedPduBuf = Buffer.concat([
      Buffer.from([0x30]),
      Buffer.from([contextEngineBuf.length + contextNameBuf.length + pduBuf.length]),
      contextEngineBuf,
      contextNameBuf,
      pduBuf
    ]);
    
    // Version
    const versionBuf = Buffer.concat([
      Buffer.from([0x02, 0x01]), // ASN.1 Integer, length 1
      Buffer.from([0x03])        // SNMP version 3 (3)
    ]);
    
    // Complete message
    return Buffer.concat([
      Buffer.from([0x30]),
      Buffer.from([versionBuf.length + msgHeaderBuf.length + securityBuf.length + scopedPduBuf.length]),
      versionBuf,
      msgHeaderBuf,
      securityBuf,
      scopedPduBuf
    ]);
  }
}