import type { ISnmpConfig } from './types.js';
import { SnmpEncoder } from './encoder.js';

/**
 * SNMP packet parsing utilities
 * Parses SNMP response packets
 */
export class SnmpPacketParser {
  /**
   * Parse an SNMP response
   * @param buffer Response buffer
   * @param config SNMP configuration
   * @param debug Whether to enable debug output
   * @returns Parsed value or null if parsing failed
   */
  public static parseSnmpResponse(buffer: Buffer, config: ISnmpConfig, debug: boolean = false): any {
    // Check if we have a response packet
    if (buffer[0] !== 0x30) {
      throw new Error('Invalid SNMP response format');
    }
    
    // For SNMPv3, we need to handle the message differently
    if (config.version === 3) {
      return this.parseSnmpV3Response(buffer, debug);
    }
    
    if (debug) {
      console.log('Parsing SNMPv1/v2 response: ', buffer.toString('hex'));
    }
    
    try {
      // Enhanced structured parsing approach
      // SEQUENCE header
      let pos = 0;
      if (buffer[pos] !== 0x30) {
        throw new Error('Missing SEQUENCE at start of response');
      }
      // Skip SEQUENCE header - assume length is in single byte for simplicity
      // In a more robust implementation, we'd handle multi-byte lengths
      pos += 2;
      
      // VERSION
      if (buffer[pos] !== 0x02) {
        throw new Error('Missing INTEGER for version');
      }
      const versionLength = buffer[pos + 1];
      pos += 2 + versionLength;
      
      // COMMUNITY STRING
      if (buffer[pos] !== 0x04) {
        throw new Error('Missing OCTET STRING for community');
      }
      const communityLength = buffer[pos + 1];
      pos += 2 + communityLength;
      
      // PDU TYPE - should be RESPONSE (0xA2)
      if (buffer[pos] !== 0xA2) {
        throw new Error(`Unexpected PDU type: 0x${buffer[pos].toString(16)}, expected 0xA2`);
      }
      // Skip PDU header
      pos += 2;
      
      // REQUEST ID
      if (buffer[pos] !== 0x02) {
        throw new Error('Missing INTEGER for request ID');
      }
      const requestIdLength = buffer[pos + 1];
      pos += 2 + requestIdLength;
      
      // ERROR STATUS
      if (buffer[pos] !== 0x02) {
        throw new Error('Missing INTEGER for error status');
      }
      const errorStatusLength = buffer[pos + 1];
      const errorStatus = SnmpEncoder.decodeInteger(buffer, pos + 2, errorStatusLength);
      
      if (errorStatus !== 0) {
        throw new Error(`SNMP error status: ${errorStatus}`);
      }
      pos += 2 + errorStatusLength;
      
      // ERROR INDEX
      if (buffer[pos] !== 0x02) {
        throw new Error('Missing INTEGER for error index');
      }
      const errorIndexLength = buffer[pos + 1];
      pos += 2 + errorIndexLength;
      
      // VARBIND LIST
      if (buffer[pos] !== 0x30) {
        throw new Error('Missing SEQUENCE for varbind list');
      }
      // Skip varbind list header
      pos += 2;
      
      // VARBIND
      if (buffer[pos] !== 0x30) {
        throw new Error('Missing SEQUENCE for varbind');
      }
      // Skip varbind header
      pos += 2;
      
      // OID
      if (buffer[pos] !== 0x06) {
        throw new Error('Missing OBJECT IDENTIFIER for OID');
      }
      const oidLength = buffer[pos + 1];
      pos += 2 + oidLength;
      
      // VALUE - this is what we want
      const valueType = buffer[pos];
      const valueLength = buffer[pos + 1];
      
      if (debug) {
        console.log(`Found value type: 0x${valueType.toString(16)}, length: ${valueLength}`);
      }
      
      return this.parseValueByType(valueType, valueLength, buffer, pos, debug);
    } catch (error) {
      if (debug) {
        console.error('Error in structured parsing:', error);
        console.error('Falling back to scan-based parsing method');
      }
      
      return this.scanBasedParsing(buffer, debug);
    }
  }

  /**
   * Parse value by ASN.1 type
   * @param valueType ASN.1 type
   * @param valueLength Value length
   * @param buffer Buffer containing the value
   * @param pos Position of the value in the buffer
   * @param debug Whether to enable debug output
   * @returns Parsed value
   */
  private static parseValueByType(
    valueType: number, 
    valueLength: number, 
    buffer: Buffer, 
    pos: number, 
    debug: boolean
  ): any {
    switch (valueType) {
      case 0x02: // INTEGER
        {
          const value = SnmpEncoder.decodeInteger(buffer, pos + 2, valueLength);
          if (debug) {
            console.log('Parsed INTEGER value:', value);
          }
          return value;
        }
        
      case 0x04: // OCTET STRING
        {
          const value = buffer.slice(pos + 2, pos + 2 + valueLength).toString();
          if (debug) {
            console.log('Parsed OCTET STRING value:', value);
          }
          return value;
        }
        
      case 0x05: // NULL
        if (debug) {
          console.log('Parsed NULL value');
        }
        return null;
        
      case 0x06: // OBJECT IDENTIFIER (rare in a value position)
        {
          // Usually this would be encoded as a string representation
          const value = buffer.slice(pos + 2, pos + 2 + valueLength).toString('hex');
          if (debug) {
            console.log('Parsed OBJECT IDENTIFIER value (hex):', value);
          }
          return value;
        }
        
      case 0x40: // IP ADDRESS
        {
          if (valueLength !== 4) {
            throw new Error(`Invalid IP address length: ${valueLength}, expected 4`);
          }
          const octets = [];
          for (let i = 0; i < 4; i++) {
            octets.push(buffer[pos + 2 + i]);
          }
          const value = octets.join('.');
          if (debug) {
            console.log('Parsed IP ADDRESS value:', value);
          }
          return value;
        }
        
      case 0x41: // COUNTER
      case 0x42: // GAUGE32
      case 0x43: // TIMETICKS
      case 0x44: // OPAQUE
        {
          // All these are essentially unsigned 32-bit integers
          const value = SnmpEncoder.decodeInteger(buffer, pos + 2, valueLength);
          if (debug) {
            console.log(`Parsed ${valueType === 0x41 ? 'COUNTER' 
                        : valueType === 0x42 ? 'GAUGE32'
                        : valueType === 0x43 ? 'TIMETICKS'
                        : 'OPAQUE'} value:`, value);
          }
          return value;
        }
        
      default:
        if (debug) {
          console.log(`Unknown value type: 0x${valueType.toString(16)}`);
        }
        return null;
    }
  }

  /**
   * Fallback scan-based parsing method
   * @param buffer Buffer containing the SNMP response
   * @param debug Whether to enable debug output
   * @returns Parsed value or null if parsing failed
   */
  private static scanBasedParsing(buffer: Buffer, debug: boolean): any {
    // Look for various data types in the response
    // The value is near the end of the packet after the OID
    
    // We're looking for one of these:
    // 0x02 - Integer - can be at the end of a varbind
    // 0x04 - OctetString
    // 0x05 - Null
    // 0x42 - Gauge32 - special type for unsigned 32-bit integers
    // 0x43 - Timeticks - special type for time values

    // This algorithm performs a thorough search for data types
    // by iterating from the start and watching for varbind structures
    
    // Walk through the buffer looking for varbinds
    let i = 0;
    
    // First, find the varbinds section (0x30 sequence)
    while (i < buffer.length - 2) {
      // Look for a varbinds sequence
      if (buffer[i] === 0x30) {
        const varbindsLength = buffer[i + 1];
        const varbindsEnd = i + 2 + varbindsLength;
        
        // Now search within the varbinds for the value
        let j = i + 2;
        while (j < varbindsEnd - 2) {
          // Look for a varbind (0x30 sequence)
          if (buffer[j] === 0x30) {
            const varbindLength = buffer[j + 1];
            const varbindEnd = j + 2 + varbindLength;
            
            // Skip over the OID and find the value within this varbind
            let k = j + 2;
            while (k < varbindEnd - 1) {
              // First find the OID
              if (buffer[k] === 0x06) { // OID
                const oidLength = buffer[k + 1];
                k += 2 + oidLength; // Skip past the OID
                
                // We should now be at the value
                // Check what type it is
                if (k < varbindEnd - 1) {
                  return this.parseValueAtPosition(buffer, k, debug);
                }
                
                // If we didn't find a value, move to next byte
                k++;
              } else {
                // Move to next byte
                k++;
              }
            }
            
            // Move to next varbind
            j = varbindEnd;
          } else {
            // Move to next byte
            j++;
          }
        }
        
        // Move to next sequence
        i = varbindsEnd;
      } else {
        // Move to next byte
        i++;
      }
    }
    
    if (debug) {
      console.log('No valid value found in SNMP response');
    }
    return null;
  }

  /**
   * Parse value at a specific position in the buffer
   * @param buffer Buffer containing the SNMP response
   * @param pos Position of the value in the buffer
   * @param debug Whether to enable debug output
   * @returns Parsed value or null if parsing failed
   */
  private static parseValueAtPosition(buffer: Buffer, pos: number, debug: boolean): any {
    if (buffer[pos] === 0x02) { // Integer
      const valueLength = buffer[pos + 1];
      const value = SnmpEncoder.decodeInteger(buffer, pos + 2, valueLength);
      if (debug) {
        console.log('Found Integer value:', value);
      }
      return value;
    } else if (buffer[pos] === 0x42) { // Gauge32
      const valueLength = buffer[pos + 1];
      const value = SnmpEncoder.decodeInteger(buffer, pos + 2, valueLength);
      if (debug) {
        console.log('Found Gauge32 value:', value);
      }
      return value;
    } else if (buffer[pos] === 0x43) { // TimeTicks
      const valueLength = buffer[pos + 1];
      const value = SnmpEncoder.decodeInteger(buffer, pos + 2, valueLength);
      if (debug) {
        console.log('Found Timeticks value:', value);
      }
      return value;
    } else if (buffer[pos] === 0x04) { // OctetString
      const valueLength = buffer[pos + 1];
      if (debug) {
        console.log('Found OctetString value');
      }
      // Just return the string value as-is
      return buffer.slice(pos + 2, pos + 2 + valueLength).toString();
    } else if (buffer[pos] === 0x05) { // Null
      if (debug) {
        console.log('Found Null value');
      }
      return null;
    }
    
    return null;
  }

  /**
   * Parse an SNMPv3 response
   * @param buffer Buffer containing the SNMP response
   * @param debug Whether to enable debug output
   * @returns Parsed value or null if parsing failed
   */
  public static parseSnmpV3Response(buffer: Buffer, debug: boolean = false): any {
    // SNMPv3 parsing is complex. In a real implementation, we would:
    // 1. Parse the header and get the security parameters
    // 2. Verify authentication if used
    // 3. Decrypt the PDU if privacy was used
    // 4. Extract the PDU and parse it
    
    if (debug) {
      console.log('Parsing SNMPv3 response: ', buffer.toString('hex'));
    }
    
    // Find the scopedPDU - it should be the last OCTET STRING in the message
    let scopedPduPos = -1;
    for (let i = buffer.length - 50; i >= 0; i--) {
      if (buffer[i] === 0x04 && buffer[i + 1] > 10) { // OCTET STRING with reasonable length
        scopedPduPos = i;
        break;
      }
    }
    
    if (scopedPduPos === -1) {
      if (debug) {
        console.log('Could not find scoped PDU in SNMPv3 response');
      }
      return null;
    }
    
    // Skip to the PDU content
    let pduContent = buffer.slice(scopedPduPos + 2); // Skip OCTET STRING header
    
    // This improved algorithm performs a more thorough search for varbinds 
    // in the scoped PDU
    
    // First, look for the response PDU (sequence with tag 0xa2)
    let responsePdu = null;
    for (let i = 0; i < pduContent.length - 3; i++) {
      if (pduContent[i] === 0xa2) {
        // Found the response PDU
        const pduLength = pduContent[i + 1];
        responsePdu = pduContent.slice(i, i + 2 + pduLength);
        break;
      }
    }
    
    if (!responsePdu) {
      // Try to find the varbinds directly
      for (let i = 0; i < pduContent.length - 3; i++) {
        if (pduContent[i] === 0x30) {
          const seqLength = pduContent[i + 1];
          if (i + 2 + seqLength <= pduContent.length) {
            // Check if this sequence might be the varbinds
            const possibleVarbinds = pduContent.slice(i, i + 2 + seqLength);
            
            // Look for varbind structure inside
            for (let j = 0; j < possibleVarbinds.length - 3; j++) {
              if (possibleVarbinds[j] === 0x30) {
                // Might be a varbind - look for an OID inside
                for (let k = j; k < j + 10 && k < possibleVarbinds.length - 1; k++) {
                  if (possibleVarbinds[k] === 0x06) {
                    // Found an OID, so this is likely the varbinds sequence
                    responsePdu = possibleVarbinds;
                    break;
                  }
                }
                
                if (responsePdu) break;
              }
            }
            
            if (responsePdu) break;
          }
        }
      }
    }
    
    if (!responsePdu) {
      if (debug) {
        console.log('Could not find response PDU in SNMPv3 response');
      }
      return null;
    }
    
    // Now that we have the response PDU, search for varbinds
    // Skip the first few bytes to get past the header fields
    let varbindsPos = -1;
    for (let i = 10; i < responsePdu.length - 3; i++) {
      if (responsePdu[i] === 0x30) {
        // Check if this is the start of the varbinds
        // by seeing if it contains a varbind sequence
        for (let j = i + 2; j < i + 10 && j < responsePdu.length - 3; j++) {
          if (responsePdu[j] === 0x30) {
            varbindsPos = i;
            break;
          }
        }
        
        if (varbindsPos !== -1) break;
      }
    }
    
    if (varbindsPos === -1) {
      if (debug) {
        console.log('Could not find varbinds in SNMPv3 response');
      }
      return null;
    }
    
    // Get the varbinds
    const varbindsLength = responsePdu[varbindsPos + 1];
    const varbinds = responsePdu.slice(varbindsPos, varbindsPos + 2 + varbindsLength);
    
    // Now search for values inside the varbinds
    for (let i = 2; i < varbinds.length - 3; i++) {
      // Look for a varbind sequence
      if (varbinds[i] === 0x30) {
        const varbindLength = varbinds[i + 1];
        const varbind = varbinds.slice(i, i + 2 + varbindLength);
        
        // Inside the varbind, look for the OID and then the value
        for (let j = 0; j < varbind.length - 3; j++) {
          if (varbind[j] === 0x06) { // OID
            const oidLength = varbind[j + 1];
            
            // The value should be right after the OID
            const valuePos = j + 2 + oidLength;
            if (valuePos < varbind.length - 1) {
              // Check what type of value it is
              if (varbind[valuePos] === 0x02) { // INTEGER
                const valueLength = varbind[valuePos + 1];
                const value = SnmpEncoder.decodeInteger(varbind, valuePos + 2, valueLength);
                if (debug) {
                  console.log('Found INTEGER value in SNMPv3 response:', value);
                }
                return value;
              } else if (varbind[valuePos] === 0x42) { // Gauge32
                const valueLength = varbind[valuePos + 1];
                const value = SnmpEncoder.decodeInteger(varbind, valuePos + 2, valueLength);
                if (debug) {
                  console.log('Found Gauge32 value in SNMPv3 response:', value);
                }
                return value;
              } else if (varbind[valuePos] === 0x43) { // TimeTicks
                const valueLength = varbind[valuePos + 1];
                const value = SnmpEncoder.decodeInteger(varbind, valuePos + 2, valueLength);
                if (debug) {
                  console.log('Found TimeTicks value in SNMPv3 response:', value);
                }
                return value;
              } else if (varbind[valuePos] === 0x04) { // OctetString
                const valueLength = varbind[valuePos + 1];
                const value = varbind.slice(valuePos + 2, valuePos + 2 + valueLength).toString();
                if (debug) {
                  console.log('Found OctetString value in SNMPv3 response:', value);
                }
                return value;
              }
            }
          }
        }
      }
    }
    
    if (debug) {
      console.log('No valid value found in SNMPv3 response');
    }
    return null;
  }

  /**
   * Extract engine ID from SNMPv3 response
   * @param buffer Buffer containing the SNMP response
   * @param debug Whether to enable debug output
   * @returns Extracted engine ID or null if extraction failed
   */
  public static extractEngineId(buffer: Buffer, debug: boolean = false): Buffer | null {
    try {
      // Simple parsing to find the engine ID
      // Look for the first octet string with appropriate length
      for (let i = 0; i < buffer.length - 10; i++) {
        if (buffer[i] === 0x04) { // Octet string
          const len = buffer[i + 1];
          if (len >= 5 && len <= 32) { // Engine IDs are typically 5-32 bytes
            // Verify this looks like an engine ID (usually starts with 0x80)
            if (buffer[i + 2] === 0x80) {
              if (debug) {
                console.log('Found engine ID at position', i);
                console.log('Engine ID:', buffer.slice(i + 2, i + 2 + len).toString('hex'));
              }
              return buffer.slice(i + 2, i + 2 + len);
            }
          }
        }
      }
      return null;
    } catch (error) {
      console.error('Error extracting engine ID:', error);
      return null;
    }
  }
}