package collector

import (
	"strings"

	"git.mchus.pro/mchus/logpile/internal/models"
)

func (r redfishSnapshotReader) enrichNICsFromNetworkInterfaces(nics *[]models.NetworkAdapter, systemPaths []string) {
	if nics == nil {
		return
	}
	bySlot := make(map[string]int, len(*nics))
	for i, nic := range *nics {
		bySlot[strings.ToLower(strings.TrimSpace(nic.Slot))] = i
	}

	for _, systemPath := range systemPaths {
		ifaces, err := r.getCollectionMembers(joinPath(systemPath, "/NetworkInterfaces"))
		if err != nil || len(ifaces) == 0 {
			continue
		}
		for _, iface := range ifaces {
			slot := firstNonEmpty(asString(iface["Id"]), asString(iface["Name"]))
			if strings.TrimSpace(slot) == "" {
				continue
			}
			idx, ok := bySlot[strings.ToLower(strings.TrimSpace(slot))]
			if !ok {
				// The NetworkInterface Id (e.g. "2") may not match the display slot of
				// the real NIC that came from Chassis/NetworkAdapters (e.g. "RISER 5
				// slot 1 (7)"). Try to find the real NIC via the Links.NetworkAdapter
				// cross-reference before creating a ghost entry.
				if linkedIdx := r.findNICIndexByLinkedNetworkAdapter(iface, *nics, bySlot); linkedIdx >= 0 {
					idx = linkedIdx
					ok = true
				}
			}
			if !ok {
				*nics = append(*nics, models.NetworkAdapter{
					Slot:    slot,
					Present: true,
					Model:   firstNonEmpty(asString(iface["Model"]), asString(iface["Name"])),
					Status:  mapStatus(iface["Status"]),
				})
				idx = len(*nics) - 1
				bySlot[strings.ToLower(strings.TrimSpace(slot))] = idx
			}

			portsPath := redfishLinkedPath(iface, "NetworkPorts")
			if portsPath == "" {
				continue
			}
			portDocs, err := r.getCollectionMembers(portsPath)
			if err != nil || len(portDocs) == 0 {
				continue
			}
			macs := append([]string{}, (*nics)[idx].MACAddresses...)
			for _, p := range portDocs {
				macs = append(macs, collectNetworkPortMACs(p)...)
			}
			(*nics)[idx].MACAddresses = dedupeStrings(macs)
			if sanitizeNetworkPortCount((*nics)[idx].PortCount) == 0 {
				(*nics)[idx].PortCount = len(portDocs)
			}
		}
	}
}

func (r redfishSnapshotReader) collectNICs(chassisPaths []string) []models.NetworkAdapter {
	var nics []models.NetworkAdapter
	for _, chassisPath := range chassisPaths {
		adapterDocs, err := r.getCollectionMembers(joinPath(chassisPath, "/NetworkAdapters"))
		if err != nil {
			continue
		}
		for _, doc := range adapterDocs {
			nics = append(nics, r.buildNICFromAdapterDoc(doc))
		}
	}
	return dedupeNetworkAdapters(nics)
}

func (r redfishSnapshotReader) buildNICFromAdapterDoc(adapterDoc map[string]interface{}) models.NetworkAdapter {
	nic := parseNIC(adapterDoc)
	adapterFunctionDocs := r.getNetworkAdapterFunctionDocs(adapterDoc)
	for _, pciePath := range networkAdapterPCIeDevicePaths(adapterDoc) {
		pcieDoc, err := r.getJSON(pciePath)
		if err != nil {
			continue
		}
		functionDocs := r.getLinkedPCIeFunctions(pcieDoc)
		for _, adapterFnDoc := range adapterFunctionDocs {
			functionDocs = append(functionDocs, r.getLinkedPCIeFunctions(adapterFnDoc)...)
		}
		functionDocs = dedupeJSONDocsByPath(functionDocs)
		supplementalDocs := r.getLinkedSupplementalDocs(pcieDoc, "EnvironmentMetrics", "Metrics")
		for _, fn := range functionDocs {
			supplementalDocs = append(supplementalDocs, r.getLinkedSupplementalDocs(fn, "EnvironmentMetrics", "Metrics")...)
		}
		enrichNICFromPCIe(&nic, pcieDoc, functionDocs, supplementalDocs)
	}
	if len(nic.MACAddresses) == 0 {
		r.enrichNICMACsFromNetworkDeviceFunctions(&nic, adapterDoc)
	}
	return nic
}

func (r redfishSnapshotReader) getNetworkAdapterFunctionDocs(adapterDoc map[string]interface{}) []map[string]interface{} {
	ndfCol, ok := adapterDoc["NetworkDeviceFunctions"].(map[string]interface{})
	if !ok {
		return nil
	}
	colPath := asString(ndfCol["@odata.id"])
	if colPath == "" {
		return nil
	}
	funcDocs, err := r.getCollectionMembers(colPath)
	if err != nil {
		return nil
	}
	return funcDocs
}

func (r redfishSnapshotReader) collectPCIeDevices(systemPaths, chassisPaths []string) []models.PCIeDevice {
	collections := make([]string, 0, len(systemPaths)+len(chassisPaths))
	for _, systemPath := range systemPaths {
		collections = append(collections, joinPath(systemPath, "/PCIeDevices"))
	}
	for _, chassisPath := range chassisPaths {
		collections = append(collections, joinPath(chassisPath, "/PCIeDevices"))
	}
	var out []models.PCIeDevice
	for _, collectionPath := range collections {
		memberDocs, err := r.getCollectionMembers(collectionPath)
		if err != nil || len(memberDocs) == 0 {
			continue
		}
		for _, doc := range memberDocs {
			functionDocs := r.getLinkedPCIeFunctions(doc)
			if looksLikeGPU(doc, functionDocs) {
				continue
			}
			if replayPCIeDeviceBackedByCanonicalNIC(doc, functionDocs) {
				continue
			}
			supplementalDocs := r.getLinkedSupplementalDocs(doc, "EnvironmentMetrics", "Metrics")
			supplementalDocs = append(supplementalDocs, r.getChassisScopedPCIeSupplementalDocs(doc)...)
			for _, fn := range functionDocs {
				supplementalDocs = append(supplementalDocs, r.getLinkedSupplementalDocs(fn, "EnvironmentMetrics", "Metrics")...)
			}
			dev := parsePCIeDeviceWithSupplementalDocs(doc, functionDocs, supplementalDocs)
			if shouldSkipReplayPCIeDevice(doc, dev) {
				continue
			}
			out = append(out, dev)
		}
	}
	for _, systemPath := range systemPaths {
		functionDocs, err := r.getCollectionMembers(joinPath(systemPath, "/PCIeFunctions"))
		if err != nil || len(functionDocs) == 0 {
			continue
		}
		for idx, fn := range functionDocs {
			supplementalDocs := r.getLinkedSupplementalDocs(fn, "EnvironmentMetrics", "Metrics")
			dev := parsePCIeFunctionWithSupplementalDocs(fn, supplementalDocs, idx+1)
			if shouldSkipReplayPCIeDevice(fn, dev) {
				continue
			}
			out = append(out, dev)
		}
	}
	return dedupePCIeDevices(out)
}

func shouldSkipReplayPCIeDevice(doc map[string]interface{}, dev models.PCIeDevice) bool {
	if isUnidentifiablePCIeDevice(dev) {
		return true
	}
	if replayNetworkFunctionBackedByCanonicalNIC(doc, dev) {
		return true
	}
	if isReplayStorageServiceEndpoint(doc, dev) {
		return true
	}
	if isReplayNoisePCIeClass(dev.DeviceClass) {
		return true
	}
	if isReplayDisplayDeviceDuplicate(doc, dev) {
		return true
	}
	return false
}

func replayPCIeDeviceBackedByCanonicalNIC(doc map[string]interface{}, functionDocs []map[string]interface{}) bool {
	if !looksLikeReplayNetworkPCIeDevice(doc, functionDocs) {
		return false
	}
	for _, fn := range functionDocs {
		if hasRedfishLinkedMember(fn, "NetworkDeviceFunctions") {
			return true
		}
	}
	return false
}

func replayNetworkFunctionBackedByCanonicalNIC(doc map[string]interface{}, dev models.PCIeDevice) bool {
	if !looksLikeReplayNetworkClass(dev.DeviceClass) {
		return false
	}
	return hasRedfishLinkedMember(doc, "NetworkDeviceFunctions")
}

func looksLikeReplayNetworkPCIeDevice(doc map[string]interface{}, functionDocs []map[string]interface{}) bool {
	for _, fn := range functionDocs {
		if looksLikeReplayNetworkClass(asString(fn["DeviceClass"])) {
			return true
		}
	}
	joined := strings.ToLower(strings.TrimSpace(strings.Join([]string{
		asString(doc["DeviceType"]),
		asString(doc["Description"]),
		asString(doc["Name"]),
		asString(doc["Model"]),
	}, " ")))
	return strings.Contains(joined, "network")
}

func looksLikeReplayNetworkClass(class string) bool {
	class = strings.ToLower(strings.TrimSpace(class))
	return strings.Contains(class, "network") || strings.Contains(class, "ethernet")
}

func isReplayStorageServiceEndpoint(doc map[string]interface{}, dev models.PCIeDevice) bool {
	class := strings.ToLower(strings.TrimSpace(dev.DeviceClass))
	if class != "massstoragecontroller" && class != "mass storage controller" {
		return false
	}
	name := strings.ToLower(strings.TrimSpace(firstNonEmpty(
		dev.PartNumber,
		asString(doc["PartNumber"]),
		asString(doc["Description"]),
	)))
	if strings.Contains(name, "pcie switch management endpoint") {
		return true
	}
	if strings.Contains(name, "volume management device nvme raid controller") {
		return true
	}
	return false
}

func hasRedfishLinkedMember(doc map[string]interface{}, key string) bool {
	links, ok := doc["Links"].(map[string]interface{})
	if !ok {
		return false
	}
	if asInt(links[key+"@odata.count"]) > 0 {
		return true
	}
	linked, ok := links[key]
	if !ok {
		return false
	}
	switch v := linked.(type) {
	case []interface{}:
		return len(v) > 0
	case map[string]interface{}:
		if asString(v["@odata.id"]) != "" {
			return true
		}
		return len(v) > 0
	default:
		return false
	}
}

func isReplayNoisePCIeClass(class string) bool {
	switch strings.ToLower(strings.TrimSpace(class)) {
	case "bridge", "processor", "signalprocessingcontroller", "signal processing controller", "serialbuscontroller", "serial bus controller":
		return true
	default:
		return false
	}
}

func isReplayDisplayDeviceDuplicate(doc map[string]interface{}, dev models.PCIeDevice) bool {
	class := strings.ToLower(strings.TrimSpace(dev.DeviceClass))
	if class != "displaycontroller" && class != "display controller" {
		return false
	}
	return strings.EqualFold(strings.TrimSpace(asString(doc["Description"])), "Display Device")
}

func (r redfishSnapshotReader) getChassisScopedPCIeSupplementalDocs(doc map[string]interface{}) []map[string]interface{} {
	docPath := normalizeRedfishPath(asString(doc["@odata.id"]))
	chassisPath := chassisPathForPCIeDoc(docPath)
	if chassisPath == "" {
		return nil
	}

	out := make([]map[string]interface{}, 0, 6)
	if looksLikeNVSwitchPCIeDoc(doc) {
		for _, path := range []string{
			joinPath(chassisPath, "/EnvironmentMetrics"),
			joinPath(chassisPath, "/ThermalSubsystem/ThermalMetrics"),
		} {
			supplementalDoc, err := r.getJSON(path)
			if err != nil || len(supplementalDoc) == 0 {
				continue
			}
			out = append(out, supplementalDoc)
		}
	}
	deviceDocs, err := r.getCollectionMembers(joinPath(chassisPath, "/Devices"))
	if err == nil {
		for _, deviceDoc := range deviceDocs {
			if !redfishPCIeMatchesChassisDeviceDoc(doc, deviceDoc) {
				continue
			}
			out = append(out, deviceDoc)
		}
	}
	return out
}

// collectBMCMAC returns the MAC address of the best BMC management interface
// found in Managers/*/EthernetInterfaces. Prefer an active link with an IP
// address over a passive sideband interface.
func (r redfishSnapshotReader) collectBMCMAC(managerPaths []string) string {
	summary := r.collectBMCManagementSummary(managerPaths)
	if len(summary) == 0 {
		return ""
	}
	return strings.ToUpper(strings.TrimSpace(asString(summary["mac_address"])))
}

func (r redfishSnapshotReader) collectBMCManagementSummary(managerPaths []string) map[string]any {
	bestScore := -1
	var best map[string]any
	for _, managerPath := range managerPaths {
		collectionPath := joinPath(managerPath, "/EthernetInterfaces")
		collectionDoc, _ := r.getJSON(collectionPath)
		ncsiEnabled, lldpMode, lldpByEth := redfishManagerEthernetCollectionHints(collectionDoc)
		members, err := r.getCollectionMembers(collectionPath)
		if err != nil || len(members) == 0 {
			continue
		}
		for _, doc := range members {
			mac := strings.TrimSpace(firstNonEmpty(
				asString(doc["PermanentMACAddress"]),
				asString(doc["MACAddress"]),
			))
			if mac == "" || strings.EqualFold(mac, "00:00:00:00:00:00") {
				continue
			}
			ifaceID := strings.TrimSpace(firstNonEmpty(asString(doc["Id"]), asString(doc["Name"])))
			summary := map[string]any{
				"manager_path":    managerPath,
				"interface_id":    ifaceID,
				"hostname":        strings.TrimSpace(asString(doc["HostName"])),
				"fqdn":            strings.TrimSpace(asString(doc["FQDN"])),
				"mac_address":     strings.ToUpper(mac),
				"link_status":     strings.TrimSpace(asString(doc["LinkStatus"])),
				"speed_mbps":      asInt(doc["SpeedMbps"]),
				"interface_name":  strings.TrimSpace(asString(doc["Name"])),
				"interface_desc":  strings.TrimSpace(asString(doc["Description"])),
				"ncsi_enabled":    ncsiEnabled,
				"lldp_mode":       lldpMode,
				"ipv4_address":    redfishManagerIPv4Field(doc, "Address"),
				"ipv4_gateway":    redfishManagerIPv4Field(doc, "Gateway"),
				"ipv4_subnet":     redfishManagerIPv4Field(doc, "SubnetMask"),
				"ipv6_address":    redfishManagerIPv6Field(doc, "Address"),
				"link_is_active":  strings.EqualFold(strings.TrimSpace(asString(doc["LinkStatus"])), "LinkActive"),
				"interface_score": 0,
			}
			if lldp, ok := lldpByEth[strings.ToLower(ifaceID)]; ok {
				summary["lldp_chassis_name"] = lldp["ChassisName"]
				summary["lldp_port_desc"] = lldp["PortDesc"]
				summary["lldp_port_id"] = lldp["PortId"]
				if vlan := asInt(lldp["VlanId"]); vlan > 0 {
					summary["lldp_vlan_id"] = vlan
				}
			}
			score := redfishManagerInterfaceScore(summary)
			summary["interface_score"] = score
			if score > bestScore {
				bestScore = score
				best = summary
			}
		}
	}
	return best
}

func redfishManagerEthernetCollectionHints(collectionDoc map[string]interface{}) (bool, string, map[string]map[string]interface{}) {
	lldpByEth := make(map[string]map[string]interface{})
	if len(collectionDoc) == 0 {
		return false, "", lldpByEth
	}
	oem, _ := collectionDoc["Oem"].(map[string]interface{})
	public, _ := oem["Public"].(map[string]interface{})
	ncsiEnabled := asBool(public["NcsiEnabled"])
	lldp, _ := public["LLDP"].(map[string]interface{})
	lldpMode := strings.TrimSpace(asString(lldp["LLDPMode"]))
	if members, ok := lldp["Members"].([]interface{}); ok {
		for _, item := range members {
			member, ok := item.(map[string]interface{})
			if !ok {
				continue
			}
			ethIndex := strings.ToLower(strings.TrimSpace(asString(member["EthIndex"])))
			if ethIndex == "" {
				continue
			}
			lldpByEth[ethIndex] = member
		}
	}
	return ncsiEnabled, lldpMode, lldpByEth
}

func redfishManagerIPv4Field(doc map[string]interface{}, key string) string {
	if len(doc) == 0 {
		return ""
	}
	for _, field := range []string{"IPv4Addresses", "IPv4StaticAddresses"} {
		list, ok := doc[field].([]interface{})
		if !ok {
			continue
		}
		for _, item := range list {
			entry, ok := item.(map[string]interface{})
			if !ok {
				continue
			}
			value := strings.TrimSpace(asString(entry[key]))
			if value != "" {
				return value
			}
		}
	}
	return ""
}

func redfishManagerIPv6Field(doc map[string]interface{}, key string) string {
	if len(doc) == 0 {
		return ""
	}
	list, ok := doc["IPv6Addresses"].([]interface{})
	if !ok {
		return ""
	}
	for _, item := range list {
		entry, ok := item.(map[string]interface{})
		if !ok {
			continue
		}
		value := strings.TrimSpace(asString(entry[key]))
		if value != "" {
			return value
		}
	}
	return ""
}

func redfishManagerInterfaceScore(summary map[string]any) int {
	score := 0
	if strings.EqualFold(strings.TrimSpace(asString(summary["link_status"])), "LinkActive") {
		score += 100
	}
	if strings.TrimSpace(asString(summary["ipv4_address"])) != "" {
		score += 40
	}
	if strings.TrimSpace(asString(summary["ipv6_address"])) != "" {
		score += 10
	}
	if strings.TrimSpace(asString(summary["mac_address"])) != "" {
		score += 10
	}
	if asInt(summary["speed_mbps"]) > 0 {
		score += 5
	}
	if ifaceID := strings.ToLower(strings.TrimSpace(asString(summary["interface_id"]))); ifaceID != "" && !strings.HasPrefix(ifaceID, "usb") {
		score += 3
	}
	if asBool(summary["ncsi_enabled"]) {
		score += 1
	}
	return score
}

// findNICIndexByLinkedNetworkAdapter resolves a NetworkInterface document to an
// existing NIC in bySlot by following Links.NetworkAdapter → the Chassis
// NetworkAdapter doc and reconstructing the canonical NIC identity. Returns -1
// if no match is found.
func (r redfishSnapshotReader) findNICIndexByLinkedNetworkAdapter(iface map[string]interface{}, existing []models.NetworkAdapter, bySlot map[string]int) int {
	links, ok := iface["Links"].(map[string]interface{})
	if !ok {
		return -1
	}
	adapterRef, ok := links["NetworkAdapter"].(map[string]interface{})
	if !ok {
		return -1
	}
	adapterPath := normalizeRedfishPath(asString(adapterRef["@odata.id"]))
	if adapterPath == "" {
		return -1
	}
	adapterDoc, err := r.getJSON(adapterPath)
	if err != nil || len(adapterDoc) == 0 {
		return -1
	}
	adapterNIC := r.buildNICFromAdapterDoc(adapterDoc)
	if serial := normalizeRedfishIdentityField(adapterNIC.SerialNumber); serial != "" {
		for idx, nic := range existing {
			if strings.EqualFold(normalizeRedfishIdentityField(nic.SerialNumber), serial) {
				return idx
			}
		}
	}
	if bdf := strings.TrimSpace(adapterNIC.BDF); bdf != "" {
		for idx, nic := range existing {
			if strings.EqualFold(strings.TrimSpace(nic.BDF), bdf) {
				return idx
			}
		}
	}
	if slot := strings.ToLower(strings.TrimSpace(adapterNIC.Slot)); slot != "" {
		if idx, ok := bySlot[slot]; ok {
			return idx
		}
	}
	for idx, nic := range existing {
		if networkAdaptersShareMACs(nic, adapterNIC) {
			return idx
		}
	}
	return -1
}

func networkAdaptersShareMACs(a, b models.NetworkAdapter) bool {
	if len(a.MACAddresses) == 0 || len(b.MACAddresses) == 0 {
		return false
	}
	seen := make(map[string]struct{}, len(a.MACAddresses))
	for _, mac := range a.MACAddresses {
		normalized := strings.ToUpper(strings.TrimSpace(mac))
		if normalized == "" {
			continue
		}
		seen[normalized] = struct{}{}
	}
	for _, mac := range b.MACAddresses {
		normalized := strings.ToUpper(strings.TrimSpace(mac))
		if normalized == "" {
			continue
		}
		if _, ok := seen[normalized]; ok {
			return true
		}
	}
	return false
}

// enrichNICMACsFromNetworkDeviceFunctions reads the NetworkDeviceFunctions
// collection linked from a NetworkAdapter document and populates the NIC's
// MACAddresses from each function's Ethernet.PermanentMACAddress / MACAddress.
// Called when PCIe-path enrichment does not produce any MACs.
func (r redfishSnapshotReader) enrichNICMACsFromNetworkDeviceFunctions(nic *models.NetworkAdapter, adapterDoc map[string]interface{}) {
	ndfCol, ok := adapterDoc["NetworkDeviceFunctions"].(map[string]interface{})
	if !ok {
		return
	}
	colPath := asString(ndfCol["@odata.id"])
	if colPath == "" {
		return
	}
	funcDocs, err := r.getCollectionMembers(colPath)
	if err != nil || len(funcDocs) == 0 {
		return
	}
	for _, fn := range funcDocs {
		eth, _ := fn["Ethernet"].(map[string]interface{})
		if eth == nil {
			continue
		}
		mac := strings.TrimSpace(firstNonEmpty(
			asString(eth["PermanentMACAddress"]),
			asString(eth["MACAddress"]),
		))
		if mac == "" {
			continue
		}
		nic.MACAddresses = dedupeStrings(append(nic.MACAddresses, strings.ToUpper(mac)))
	}
	if len(funcDocs) > 0 && nic.PortCount == 0 {
		nic.PortCount = sanitizeNetworkPortCount(len(funcDocs))
	}
}
