import Foundation /// Pre-existing configuration on a physical port that the Setup wizard's LAN step would silently /// override if the user picks that port for a new LAN/DHCP network — surfaced instead so the user /// can either choose a different, actually-free port or explicitly (twice-confirmed) opt in to /// clearing it. `DhcpServerCommandBuilder` already unconditionally detaches a port from any bridge /// as a safety net (see its own doc comment) — this check runs *before* that, to make the same /// fact visible up front instead of only implicitly, and additionally covers cases that builder /// doesn't touch at all (an existing IP address, or the port already being used as a WAN dial-up). struct PortConflict: Equatable { enum Reason: Equatable { case bridgeMember(bridgeName: String) case hasAddresses([String]) case dhcpClient case pppoeClient var description: String { switch self { case .bridgeMember(let bridgeName): return "Ist Mitglied der Bridge \"\(bridgeName)\"" case .hasAddresses(let addresses): return "Trägt bereits die IP-Adresse\(addresses.count == 1 ? "" : "n") \(addresses.joined(separator: ", "))" case .dhcpClient: return "Ist als WAN-DHCP-Client konfiguriert (bezieht selbst eine Adresse aus dem Internet)" case .pppoeClient: return "Wird von einer PPPoE-Einwahl verwendet" } } } let interfaceName: String let reasons: [Reason] /// Commands that clear each found conflict so the port is actually free before /// `DhcpServerCommandBuilder`'s own commands run. Bridge membership is deliberately excluded /// here — `DhcpServerCommandBuilder` already removes it unconditionally regardless of whether /// this check ran or was acknowledged, so repeating it would just be a harmless duplicate /// `.remove` at best; excluding it keeps this list to exactly what wouldn't otherwise happen. func resolutionCommands() -> [RouterOSCommand] { reasons.flatMap { reason -> [RouterOSCommand] in switch reason { case .bridgeMember: return [] case .hasAddresses: return [RouterOSCommand.remove( menuPath: "/ip address", restPath: "ip/address", matchField: "interface", matchValue: interfaceName, summary: "Bestehende IP-Adresse(n) auf \(interfaceName) entfernen" )] case .dhcpClient: return [RouterOSCommand.remove( menuPath: "/ip dhcp-client", restPath: "ip/dhcp-client", matchField: "interface", matchValue: interfaceName, summary: "WAN-DHCP-Client auf \(interfaceName) entfernen" )] case .pppoeClient: return [RouterOSCommand.remove( menuPath: "/interface pppoe-client", restPath: "interface/pppoe-client", matchField: "interface", matchValue: interfaceName, summary: "PPPoE-Einwahl auf \(interfaceName) entfernen" )] } } } /// `resolutionCommands()` alone for the Setup-Wizard context, where `DhcpServerCommandBuilder` /// already unconditionally detaches the port from any bridge as its own separate safety net — /// see that method's doc comment. Contexts without that separate detach (the Experte tab's /// generic "Port freimachen?" flow, applied to whatever menu the user is actually editing, not /// specifically the LAN/DHCP command set) need the bridge-membership removal included here /// instead, or acknowledging a bridge-member conflict there would silently do nothing for /// that specific reason. func resolutionCommandsIncludingBridgeDetach() -> [RouterOSCommand] { let bridgeDetach: [RouterOSCommand] = reasons.contains(where: { if case .bridgeMember = $0 { return true } else { return false } }) ? [RouterOSCommand.remove( menuPath: "/interface bridge port", restPath: "interface/bridge/port", matchField: "interface", matchValue: interfaceName, summary: "\(interfaceName) aus Bridge lösen" )] : [] return bridgeDetach + resolutionCommands() } }