// Package snmpmod polls SNMP targets for device information.
// Supports generic devices, printers (toner/pages) and switches.
package snmpmod

import (
	"context"
	"fmt"
	"log/slog"
	"strconv"
	"strings"
	"time"

	"github.com/gosnmp/gosnmp"

	"github.com/cereda-systems/nexus-scanner/internal/config"
	"github.com/cereda-systems/nexus-scanner/internal/db"
)

const modName = "snmp"

// OIDs queried for every device.
var baseOIDs = map[string]string{
	"sysDescr":    "1.3.6.1.2.1.1.1.0",
	"sysUpTime":   "1.3.6.1.2.1.1.3.0",
	"sysName":     "1.3.6.1.2.1.1.5.0",
	"sysLocation": "1.3.6.1.2.1.1.6.0",
	"sysContact":  "1.3.6.1.2.1.1.4.0",
	"ifNumber":    "1.3.6.1.2.1.2.1.0",
}

// Switch-specific scalar OIDs (queried via Get).
var switchScalarOIDs = map[string]string{
	"cpuLoad": "1.3.6.1.2.1.25.3.3.1.2.1", // hrProcessorLoad (first CPU)
}

// Printer-specific OIDs (Kyocera / most RFC 3805 printers).
var printerOIDs = map[string]string{
	"tonerLevel":    "1.3.6.1.2.1.43.11.1.1.9.1.1",
	"tonerMax":      "1.3.6.1.2.1.43.11.1.1.8.1.1",
	"pageCount":     "1.3.6.1.2.1.43.10.2.1.4.1.1",
	"printerStatus": "1.3.6.1.2.1.25.3.5.1.1.1",
}

// Walk base OIDs for interfaces and memory.
const (
	oidIfOperStatus  = "1.3.6.1.2.1.2.2.1.8"    // 1=up, 2=down
	oidIfHCInOctets  = "1.3.6.1.2.1.31.1.1.1.6"  // 64-bit in bytes
	oidIfHCOutOctets = "1.3.6.1.2.1.31.1.1.1.10" // 64-bit out bytes
	oidIfDescr       = "1.3.6.1.2.1.2.2.1.2"     // interface name
	oidHrStorageDescr = "1.3.6.1.2.1.25.2.3.1.2"
	oidHrStorageUsed  = "1.3.6.1.2.1.25.2.3.1.6"
	oidHrStorageSize  = "1.3.6.1.2.1.25.2.3.1.5"
	oidHrStorageAlloc = "1.3.6.1.2.1.25.2.3.1.4"
)

// Module polls SNMP targets and stores results.
type Module struct {
	site  string
	cfg   config.SNMPConfig
	store *db.Store
}

// New creates a new SNMP module.
func New(site string, cfg config.SNMPConfig, store *db.Store) *Module {
	return &Module{site: site, cfg: cfg, store: store}
}

func (m *Module) Name() string            { return modName }
func (m *Module) Interval() time.Duration { return m.cfg.Interval }

// Run polls all enabled SNMP targets.
func (m *Module) Run(ctx context.Context) error {
	targets, err := m.store.ListSNMPTargets()
	if err != nil {
		return fmt.Errorf("list snmp targets: %w", err)
	}
	if len(targets) == 0 {
		return nil
	}

	scanID, err := m.store.BeginScan(modName)
	if err != nil {
		return fmt.Errorf("begin scan: %w", err)
	}

	ok := 0
	for _, t := range targets {
		if !t.Enabled {
			continue
		}
		if err := m.pollTarget(ctx, t); err != nil {
			slog.Warn("snmp poll failed", "target", t.Name, "ip", t.IP, "err", err)
		} else {
			ok++
		}
	}

	_ = m.store.EndScan(scanID, nil)
	slog.Info("snmp poll complete", "ok", ok, "total", len(targets))
	return nil
}

func (m *Module) pollTarget(ctx context.Context, t db.SNMPTarget) error {
	community := t.Community
	if community == "" {
		community = m.cfg.Community
	}

	gs := &gosnmp.GoSNMP{
		Target:    t.IP,
		Port:      161,
		Community: community,
		Version:   snmpVersion(t.Version),
		Timeout:   10 * time.Second,
		Retries:   1,
		MaxOids:   60,
	}
	if err := gs.Connect(); err != nil {
		return fmt.Errorf("connect: %w", err)
	}
	defer gs.Conn.Close()

	now := time.Now()

	// --- Base + type-specific scalar GET ---
	oids := make(map[string]string)
	for k, v := range baseOIDs {
		oids[k] = v
	}
	switch t.Type {
	case "printer":
		for k, v := range printerOIDs {
			oids[k] = v
		}
	case "switch":
		for k, v := range switchScalarOIDs {
			oids[k] = v
		}
	}

	oidList := make([]string, 0, len(oids))
	reverseMap := make(map[string]string)
	for name, oid := range oids {
		oidList = append(oidList, oid)
		reverseMap[oid] = name
	}

	result, err := gs.Get(oidList)
	if err != nil {
		return fmt.Errorf("get: %w", err)
	}

	for _, pdu := range result.Variables {
		name := resolveOID(pdu.Name, reverseMap)
		if name == "" {
			continue
		}
		value := pduToString(pdu)
		if value == "" || value == "<nil>" || strings.HasPrefix(value, "<nil>") {
			continue
		}
		_ = m.store.UpsertSNMPResult(db.SNMPResult{
			TargetID:  t.ID,
			OIDName:   name,
			Value:     value,
			ScannedAt: now,
		})
	}

	// --- Switch: walk interface table + memory ---
	if t.Type == "switch" {
		m.walkSwitch(gs, t, now)
	}

	slog.Debug("snmp polled", "target", t.Name, "ip", t.IP, "type", t.Type)
	return nil
}

// walkSwitch collects interface stats and memory for switch-type targets.
func (m *Module) walkSwitch(gs *gosnmp.GoSNMP, t db.SNMPTarget, now time.Time) {
	// Interface status walk → portsUp / portsTotal
	var portsUp, portsTotal int
	var trafficIn, trafficOut uint64

	ifStatus := walkOID(gs, oidIfOperStatus)
	for _, v := range ifStatus {
		portsTotal++
		if v == "1" {
			portsUp++
		}
	}

	// Traffic counters (64-bit)
	for _, v := range walkOID(gs, oidIfHCInOctets) {
		n, _ := strconv.ParseUint(v, 10, 64)
		trafficIn += n
	}
	for _, v := range walkOID(gs, oidIfHCOutOctets) {
		n, _ := strconv.ParseUint(v, 10, 64)
		trafficOut += n
	}

	save := func(name, value string) {
		_ = m.store.UpsertSNMPResult(db.SNMPResult{
			TargetID:  t.ID,
			OIDName:   name,
			Value:     value,
			ScannedAt: now,
		})
	}

	if portsTotal > 0 {
		save("portsTotal", strconv.Itoa(portsTotal))
		save("portsUp", strconv.Itoa(portsUp))
	}
	if trafficIn > 0 {
		save("trafficIn", strconv.FormatUint(trafficIn, 10))
	}
	if trafficOut > 0 {
		save("trafficOut", strconv.FormatUint(trafficOut, 10))
	}

	// Memory: find "Physical Memory" or "Real Memory" row
	descrByIdx := walkOID(gs, oidHrStorageDescr)
	usedByIdx := walkOID(gs, oidHrStorageUsed)
	sizeByIdx := walkOID(gs, oidHrStorageSize)
	allocByIdx := walkOID(gs, oidHrStorageAlloc)

	for idx, descr := range descrByIdx {
		d := strings.ToLower(descr)
		if strings.Contains(d, "physical") || strings.Contains(d, "real") || strings.Contains(d, "ram") {
			used, _ := strconv.ParseInt(usedByIdx[idx], 10, 64)
			size, _ := strconv.ParseInt(sizeByIdx[idx], 10, 64)
			alloc, _ := strconv.ParseInt(allocByIdx[idx], 10, 64)
			if alloc <= 0 {
				alloc = 1024
			}
			usedBytes := used * alloc
			totalBytes := size * alloc
			if totalBytes > 0 {
				save("memUsed", strconv.FormatInt(usedBytes, 10))
				save("memTotal", strconv.FormatInt(totalBytes, 10))
			}
			break
		}
	}
}

// walkOID performs an SNMP walk and returns a map of last-OID-index → value string.
func walkOID(gs *gosnmp.GoSNMP, baseOID string) map[string]string {
	out := make(map[string]string)
	err := gs.Walk(baseOID, func(pdu gosnmp.SnmpPDU) error {
		// Extract the index (last component of OID)
		name := strings.TrimPrefix(pdu.Name, ".")
		parts := strings.Split(name, ".")
		idx := parts[len(parts)-1]
		val := pduRawToString(pdu)
		if val != "" {
			out[idx] = val
		}
		return nil
	})
	if err != nil {
		slog.Debug("snmp walk failed", "oid", baseOID, "err", err)
	}
	return out
}

func resolveOID(oidName string, reverseMap map[string]string) string {
	key := strings.TrimPrefix(oidName, ".")
	if name := reverseMap[key]; name != "" {
		return name
	}
	// Try without trailing .0
	if len(key) > 2 && key[len(key)-2:] == ".0" {
		return reverseMap[key[:len(key)-2]]
	}
	return ""
}

func snmpVersion(v string) gosnmp.SnmpVersion {
	switch v {
	case "v1":
		return gosnmp.Version1
	case "v3":
		return gosnmp.Version3
	default:
		return gosnmp.Version2c
	}
}

// pduToString converts a PDU value to a display string (formatted).
func pduToString(pdu gosnmp.SnmpPDU) string {
	switch pdu.Type {
	case gosnmp.OctetString:
		if b, ok := pdu.Value.([]byte); ok {
			return strings.TrimSpace(string(b))
		}
	case gosnmp.TimeTicks:
		ticks, _ := pdu.Value.(uint32)
		d := time.Duration(ticks) * 10 * time.Millisecond
		h := int(d.Hours())
		mn := int(d.Minutes()) % 60
		return strconv.Itoa(h) + "h " + strconv.Itoa(mn) + "m"
	case gosnmp.ObjectIdentifier:
		if s, ok := pdu.Value.(string); ok {
			return s
		}
	default:
		return fmt.Sprintf("%v", pdu.Value)
	}
	return fmt.Sprintf("%v", pdu.Value)
}

// pduRawToString converts a PDU value to a raw numeric/string value (for walks).
func pduRawToString(pdu gosnmp.SnmpPDU) string {
	switch pdu.Type {
	case gosnmp.OctetString:
		if b, ok := pdu.Value.([]byte); ok {
			return strings.TrimSpace(string(b))
		}
	case gosnmp.Integer:
		return fmt.Sprintf("%d", pdu.Value)
	case gosnmp.Counter32, gosnmp.Gauge32, gosnmp.Uinteger32:
		return fmt.Sprintf("%d", pdu.Value)
	case gosnmp.Counter64:
		return fmt.Sprintf("%d", pdu.Value)
	case gosnmp.TimeTicks:
		if t, ok := pdu.Value.(uint32); ok {
			return fmt.Sprintf("%d", t)
		}
	}
	return fmt.Sprintf("%v", pdu.Value)
}
