// Copyright 2025, IPng Networks GmbH, Pim van Pelt <pim@ipng.ch>

package ifmib

import (
	"fmt"
	"os"
	"sync"
	"time"

	"github.com/posteo/go-agentx"
	"github.com/posteo/go-agentx/pdu"
	"github.com/posteo/go-agentx/value"
	"go.fd.io/govpp/api"
	"gopkg.in/yaml.v3"
	
	"govpp-snmp-agentx/logger"
	"govpp-snmp-agentx/vppstats"
)

// IF-MIB OID bases:
// ifEntry (classic): 1.3.6.1.2.1.2.2.1
// ifXTable (extended): 1.3.6.1.2.1.31.1.1.1

// ifEntry (1.3.6.1.2.1.2.2.1) - Classic Interface Table:
// ifIndex        .1  - Integer32
// ifDescr        .2  - DisplayString
// ifType         .3  - IANAifType
// ifMtu          .4  - Integer32
// ifSpeed        .5  - Gauge32
// ifPhysAddress  .6  - PhysAddress
// ifAdminStatus  .7  - INTEGER
// ifOperStatus   .8  - INTEGER
// ifLastChange   .9  - TimeTicks
// ifInOctets     .10 - Counter32
// ifInUcastPkts  .11 - Counter32
// ifInNUcastPkts .12 - Counter32
// ifInDiscards   .13 - Counter32
// ifInErrors     .14 - Counter32
// ifInUnknownProtos .15 - Counter32
// ifOutOctets    .16 - Counter32
// ifOutUcastPkts .17 - Counter32
// ifOutNUcastPkts .18 - Counter32
// ifOutDiscards  .19 - Counter32
// ifOutErrors    .20 - Counter32
// ifOutQLen      .21 - Gauge32
// ifSpecific     .22 - OBJECT IDENTIFIER

// ifXTable (1.3.6.1.2.1.31.1.1.1) - Extended Interface Table:
// ifName              .1  - DisplayString
// ifInMulticastPkts   .2  - Counter32
// ifInBroadcastPkts   .3  - Counter32
// ifOutMulticastPkts  .4  - Counter32
// ifOutBroadcastPkts  .5  - Counter32
// ifHCInOctets        .6  - Counter64
// ifHCInUcastPkts     .7  - Counter64
// ifHCInMulticastPkts .8  - Counter64
// ifHCInBroadcastPkts .9  - Counter64
// ifHCOutOctets       .10 - Counter64
// ifHCOutUcastPkts    .11 - Counter64
// ifHCOutMulticastPkts .12 - Counter64
// ifHCOutBroadcastPkts .13 - Counter64
// ifAlias             .18 - DisplayString

const ifEntryOID = "1.3.6.1.2.1.2.2.1"
const ifXTableOID = "1.3.6.1.2.1.31.1.1.1"

// VPP Config YAML structures
type VPPConfig struct {
	Interfaces map[string]VPPInterface `yaml:"interfaces"`
	Loopbacks  map[string]VPPInterface `yaml:"loopbacks"`
}

type VPPInterface struct {
	Description    string                    `yaml:"description"`
	SubInterfaces  map[string]VPPInterface   `yaml:"sub-interfaces"`
}

type InterfaceMIB struct {
	mutex           sync.RWMutex
	handler         *agentx.ListHandler
	ifEntrySession  *agentx.Session
	ifXTableSession *agentx.Session
	stats           map[uint32]*api.InterfaceCounters // indexed by interface index
	descriptions    map[string]string                 // interface name -> description mapping
}

func NewInterfaceMIB() *InterfaceMIB {
	return &InterfaceMIB{
		handler:      &agentx.ListHandler{},
		stats:        make(map[uint32]*api.InterfaceCounters),
		descriptions: make(map[string]string),
	}
}

func (m *InterfaceMIB) GetHandler() *agentx.ListHandler {
	return m.handler
}

func (m *InterfaceMIB) LoadVPPConfig(configPath string) error {
	m.mutex.Lock()
	defer m.mutex.Unlock()

	// Read YAML file
	data, err := os.ReadFile(configPath)
	if err != nil {
		return fmt.Errorf("failed to read VPP config file: %v", err)
	}

	// Parse YAML
	var config VPPConfig
	if err := yaml.Unmarshal(data, &config); err != nil {
		return fmt.Errorf("failed to parse VPP config YAML: %v", err)
	}

	// Extract interface descriptions
	for ifName, ifConfig := range config.Interfaces {
		if ifConfig.Description != "" {
			m.descriptions[ifName] = ifConfig.Description
			logger.Debugf("Loaded description for interface %s: %s", ifName, ifConfig.Description)
		}

		// Process sub-interfaces
		for subID, subConfig := range ifConfig.SubInterfaces {
			if subConfig.Description != "" {
				subIfName := fmt.Sprintf("%s.%s", ifName, subID)
				m.descriptions[subIfName] = subConfig.Description
				logger.Debugf("Loaded description for sub-interface %s: %s", subIfName, subConfig.Description)
			}
		}
	}

	// Extract loopback descriptions
	for ifName, ifConfig := range config.Loopbacks {
		if ifConfig.Description != "" {
			m.descriptions[ifName] = ifConfig.Description
			logger.Debugf("Loaded description for loopback %s: %s", ifName, ifConfig.Description)
		}
	}

	logger.Printf("Loaded %d interface descriptions from VPP config", len(m.descriptions))
	return nil
}

func (m *InterfaceMIB) UpdateStats(interfaceStats *api.InterfaceStats) {
	m.mutex.Lock()
	defer m.mutex.Unlock()

	logger.Debugf("Updating IF-MIB with %d interfaces", len(interfaceStats.Interfaces))

	// Clear existing entries
	m.handler = &agentx.ListHandler{}
	m.stats = make(map[uint32]*api.InterfaceCounters)

	// Add new entries
	for _, iface := range interfaceStats.Interfaces {
		logger.Debugf("Processing interface %d (%s)", iface.InterfaceIndex, iface.InterfaceName)
		m.stats[iface.InterfaceIndex] = &iface
		m.addInterfaceToMIB(&iface)
	}

	// Update both sessions with the new handler
	if m.ifEntrySession != nil {
		m.ifEntrySession.Handler = m.handler
	}
	if m.ifXTableSession != nil {
		m.ifXTableSession.Handler = m.handler
		logger.Printf("Updated session handlers with new IF-MIB data")
	}

	logger.Debugf("IF-MIB now contains %d interfaces", len(m.stats))
}

func (m *InterfaceMIB) addInterfaceToMIB(iface *api.InterfaceCounters) {
	idx := int(iface.InterfaceIndex) + *vppstats.IfIndexOffset

	// Add ifEntry (classic interface table) entries
	m.addIfEntry(iface, idx)

	// Add ifXTable (extended interface table) entries
	m.addIfXTable(iface, idx)

	logger.Debugf("Added interface %d (%s) to IF-MIB with SNMP index %d", iface.InterfaceIndex, iface.InterfaceName, idx)
}

func (m *InterfaceMIB) addIfEntry(iface *api.InterfaceCounters, idx int) {
	var item *agentx.ListItem

	// ifIndex (.1)
	item = m.handler.Add(fmt.Sprintf("%s.1.%d", ifEntryOID, idx))
	item.Type = pdu.VariableTypeInteger
	item.Value = int32(idx)

	// ifDescr (.2)
	item = m.handler.Add(fmt.Sprintf("%s.2.%d", ifEntryOID, idx))
	item.Type = pdu.VariableTypeOctetString
	item.Value = iface.InterfaceName

	// ifType (.3) - Using ethernetCsmacd(6) as default
	item = m.handler.Add(fmt.Sprintf("%s.3.%d", ifEntryOID, idx))
	item.Type = pdu.VariableTypeInteger
	item.Value = int32(6)

	// ifMtu (.4) - Default MTU 1500
	item = m.handler.Add(fmt.Sprintf("%s.4.%d", ifEntryOID, idx))
	item.Type = pdu.VariableTypeInteger
	item.Value = int32(1500)

	// ifSpeed (.5) - Default to 1Gbps (1000000000 bits/sec)
	item = m.handler.Add(fmt.Sprintf("%s.5.%d", ifEntryOID, idx))
	item.Type = pdu.VariableTypeGauge32
	item.Value = uint32(1000000000)

	// ifPhysAddress (.6) - Empty for now
	item = m.handler.Add(fmt.Sprintf("%s.6.%d", ifEntryOID, idx))
	item.Type = pdu.VariableTypeOctetString
	item.Value = ""

	// ifAdminStatus (.7) - up(1)
	item = m.handler.Add(fmt.Sprintf("%s.7.%d", ifEntryOID, idx))
	item.Type = pdu.VariableTypeInteger
	item.Value = int32(1)

	// ifOperStatus (.8) - up(1)
	item = m.handler.Add(fmt.Sprintf("%s.8.%d", ifEntryOID, idx))
	item.Type = pdu.VariableTypeInteger
	item.Value = int32(1)

	// ifLastChange (.9) - 0 (unknown)
	item = m.handler.Add(fmt.Sprintf("%s.9.%d", ifEntryOID, idx))
	item.Type = pdu.VariableTypeTimeTicks
	item.Value = 0 * time.Second

	// ifInOctets (.10)
	item = m.handler.Add(fmt.Sprintf("%s.10.%d", ifEntryOID, idx))
	item.Type = pdu.VariableTypeCounter32
	item.Value = uint32(iface.Rx.Bytes)

	// ifInUcastPkts (.11)
	item = m.handler.Add(fmt.Sprintf("%s.11.%d", ifEntryOID, idx))
	item.Type = pdu.VariableTypeCounter32
	item.Value = uint32(iface.RxUnicast.Packets)

	// ifInNUcastPkts (.12) - multicast + broadcast
	item = m.handler.Add(fmt.Sprintf("%s.12.%d", ifEntryOID, idx))
	item.Type = pdu.VariableTypeCounter32
	item.Value = uint32(iface.RxMulticast.Packets + iface.RxBroadcast.Packets)

	// ifInDiscards (.13) - using drops
	item = m.handler.Add(fmt.Sprintf("%s.13.%d", ifEntryOID, idx))
	item.Type = pdu.VariableTypeCounter32
	item.Value = uint32(iface.Drops)

	// ifInErrors (.14)
	item = m.handler.Add(fmt.Sprintf("%s.14.%d", ifEntryOID, idx))
	item.Type = pdu.VariableTypeCounter32
	item.Value = uint32(iface.RxErrors)

	// ifInUnknownProtos (.15) - 0 (not available)
	item = m.handler.Add(fmt.Sprintf("%s.15.%d", ifEntryOID, idx))
	item.Type = pdu.VariableTypeCounter32
	item.Value = uint32(0)

	// ifOutOctets (.16)
	item = m.handler.Add(fmt.Sprintf("%s.16.%d", ifEntryOID, idx))
	item.Type = pdu.VariableTypeCounter32
	item.Value = uint32(iface.Tx.Bytes)

	// ifOutUcastPkts (.17)
	item = m.handler.Add(fmt.Sprintf("%s.17.%d", ifEntryOID, idx))
	item.Type = pdu.VariableTypeCounter32
	item.Value = uint32(iface.TxUnicast.Packets)

	// ifOutNUcastPkts (.18) - multicast + broadcast
	item = m.handler.Add(fmt.Sprintf("%s.18.%d", ifEntryOID, idx))
	item.Type = pdu.VariableTypeCounter32
	item.Value = uint32(iface.TxMulticast.Packets + iface.TxBroadcast.Packets)

	// ifOutDiscards (.19) - 0 (not available)
	item = m.handler.Add(fmt.Sprintf("%s.19.%d", ifEntryOID, idx))
	item.Type = pdu.VariableTypeCounter32
	item.Value = uint32(0)

	// ifOutErrors (.20)
	item = m.handler.Add(fmt.Sprintf("%s.20.%d", ifEntryOID, idx))
	item.Type = pdu.VariableTypeCounter32
	item.Value = uint32(iface.TxErrors)

	// ifOutQLen (.21) - 0 (not available)
	item = m.handler.Add(fmt.Sprintf("%s.21.%d", ifEntryOID, idx))
	item.Type = pdu.VariableTypeGauge32
	item.Value = uint32(0)

	// ifSpecific (.22) - Skip this field as it's optional and causing issues
}

func (m *InterfaceMIB) addIfXTable(iface *api.InterfaceCounters, idx int) {
	var item *agentx.ListItem

	// ifName (.1)
	item = m.handler.Add(fmt.Sprintf("%s.1.%d", ifXTableOID, idx))
	item.Type = pdu.VariableTypeOctetString
	item.Value = iface.InterfaceName

	// ifInMulticastPkts (.2)
	item = m.handler.Add(fmt.Sprintf("%s.2.%d", ifXTableOID, idx))
	item.Type = pdu.VariableTypeCounter32
	item.Value = uint32(iface.RxMulticast.Packets)

	// ifInBroadcastPkts (.3)
	item = m.handler.Add(fmt.Sprintf("%s.3.%d", ifXTableOID, idx))
	item.Type = pdu.VariableTypeCounter32
	item.Value = uint32(iface.RxBroadcast.Packets)

	// ifOutMulticastPkts (.4)
	item = m.handler.Add(fmt.Sprintf("%s.4.%d", ifXTableOID, idx))
	item.Type = pdu.VariableTypeCounter32
	item.Value = uint32(iface.TxMulticast.Packets)

	// ifOutBroadcastPkts (.5)
	item = m.handler.Add(fmt.Sprintf("%s.5.%d", ifXTableOID, idx))
	item.Type = pdu.VariableTypeCounter32
	item.Value = uint32(iface.TxBroadcast.Packets)

	// ifHCInOctets (.6)
	item = m.handler.Add(fmt.Sprintf("%s.6.%d", ifXTableOID, idx))
	item.Type = pdu.VariableTypeCounter64
	item.Value = iface.Rx.Bytes

	// ifHCInUcastPkts (.7)
	item = m.handler.Add(fmt.Sprintf("%s.7.%d", ifXTableOID, idx))
	item.Type = pdu.VariableTypeCounter64
	item.Value = iface.RxUnicast.Packets

	// ifHCInMulticastPkts (.8)
	item = m.handler.Add(fmt.Sprintf("%s.8.%d", ifXTableOID, idx))
	item.Type = pdu.VariableTypeCounter64
	item.Value = iface.RxMulticast.Packets

	// ifHCInBroadcastPkts (.9)
	item = m.handler.Add(fmt.Sprintf("%s.9.%d", ifXTableOID, idx))
	item.Type = pdu.VariableTypeCounter64
	item.Value = iface.RxBroadcast.Packets

	// ifHCOutOctets (.10)
	item = m.handler.Add(fmt.Sprintf("%s.10.%d", ifXTableOID, idx))
	item.Type = pdu.VariableTypeCounter64
	item.Value = iface.Tx.Bytes

	// ifHCOutUcastPkts (.11)
	item = m.handler.Add(fmt.Sprintf("%s.11.%d", ifXTableOID, idx))
	item.Type = pdu.VariableTypeCounter64
	item.Value = iface.TxUnicast.Packets

	// ifHCOutMulticastPkts (.12)
	item = m.handler.Add(fmt.Sprintf("%s.12.%d", ifXTableOID, idx))
	item.Type = pdu.VariableTypeCounter64
	item.Value = iface.TxMulticast.Packets

	// ifHCOutBroadcastPkts (.13)
	item = m.handler.Add(fmt.Sprintf("%s.13.%d", ifXTableOID, idx))
	item.Type = pdu.VariableTypeCounter64
	item.Value = iface.TxBroadcast.Packets

	// ifAlias (.18) - Interface description/alias
	item = m.handler.Add(fmt.Sprintf("%s.18.%d", ifXTableOID, idx))
	item.Type = pdu.VariableTypeOctetString
	// Use description from VPP config if available, otherwise use interface name
	if desc, exists := m.descriptions[iface.InterfaceName]; exists {
		item.Value = desc
	} else {
		item.Value = iface.InterfaceName
	}
}

func (m *InterfaceMIB) RegisterWithClient(client *agentx.Client) error {
	m.mutex.Lock()
	defer m.mutex.Unlock()

	// Create separate sessions for each MIB
	ifEntrySession, err := client.Session()
	if err != nil {
		return fmt.Errorf("failed to create ifEntry session: %v", err)
	}

	ifXTableSession, err := client.Session()
	if err != nil {
		return fmt.Errorf("failed to create ifXTable session: %v", err)
	}

	m.ifEntrySession = ifEntrySession
	m.ifXTableSession = ifXTableSession

	// Set handlers for both sessions
	ifEntrySession.Handler = m.handler
	ifXTableSession.Handler = m.handler

	// Register the classic ifEntry
	err = ifEntrySession.Register(127, value.MustParseOID(ifEntryOID))
	if err != nil {
		return fmt.Errorf("failed to register ifEntry: %v", err)
	}

	// Register the extended ifXTable
	err = ifXTableSession.Register(127, value.MustParseOID(ifXTableOID))
	if err != nil {
		return fmt.Errorf("failed to register ifXTable: %v", err)
	}

	logger.Debugf("Registered IF-MIB ifEntry at OID %s", ifEntryOID)
	logger.Debugf("Registered IF-MIB ifXTable at OID %s", ifXTableOID)
	return nil
}