//===----------------------------------------------------------------------===//
// Copyright © 2026 Apple Inc. and the Containerization project authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//   https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//===----------------------------------------------------------------------===//

#if os(Linux)
import ContainerizationError
import ContainerizationExtras
import ContainerizationNetlink
import Foundation
import Logging

#if canImport(Musl)
import Musl
#elseif canImport(Glibc)
import Glibc
#endif

/// Linux-only host plumbing for a container bridge network.
///
/// `create()` is idempotent: it brings the bridge to a known state (created
/// if absent, configured if already present), records what it changed in
/// `/run/containerization/bridge-<name>.state`, and `delete()` reverses
/// only what was recorded.
///
/// **NAT is opt-in.** With the default (`enableNAT: false`) `create()` only
/// brings up the bridge link and assigns the gateway IP — it does NOT touch
/// `ip_forward`, does NOT program iptables, and does NOT pick an egress
/// interface. Containers attached to the bridge can talk to each other and
/// to the host, but not to the outside world. Pass `enableNAT: true` to
/// also enable IPv4 forwarding and program a scoped MASQUERADE/FORWARD
/// pair (`-i <bridge> -o <egress>`); the bridge becomes a NAT exit and the
/// host now routes guest traffic.
///
/// Concurrent `create()`/`delete()` calls (e.g. from two `cctl run`
/// processes) serialize via `flock(LOCK_EX)` on
/// `/run/containerization/bridge-<name>.lock`.
///
/// Requires root (or `CAP_NET_ADMIN` plus, when NAT is enabled, the ability
/// to write `/proc/sys/...` and invoke `iptables`).
public struct BridgeManager: Sendable {
    public let name: String
    public let subnet: CIDRv4
    public let gateway: IPv4Address
    public let mtu: UInt32
    public let egressInterface: String?
    public let enableNAT: Bool
    private let log: Logger

    /// - Parameters:
    ///   - name: bridge interface name (e.g. `cz0`).
    ///   - subnet: subnet to assign on the bridge.
    ///   - gateway: host-side IP on the bridge. Defaults to `subnet.gateway`
    ///     (= `subnet.lower + 1`).
    ///   - mtu: bridge MTU. Default 1500.
    ///   - egressInterface: explicit egress iface for MASQUERADE. nil =
    ///     auto-detect via `/proc/net/route` at `create()` time. Only used
    ///     when `enableNAT` is true.
    ///   - enableNAT: when true, program iptables MASQUERADE+FORWARD and
    ///     enable `net.ipv4.ip_forward`. Default false — the bridge is
    ///     created without external connectivity, leaving host firewall
    ///     policy untouched.
    ///   - logger: optional logger. Defaults to a `bridge`-labeled logger.
    public init(
        name: String,
        subnet: CIDRv4,
        gateway: IPv4Address? = nil,
        mtu: UInt32 = 1500,
        egressInterface: String? = nil,
        enableNAT: Bool = false,
        logger: Logger? = nil
    ) {
        self.name = Self.validateInterfaceName(name)
        self.subnet = subnet
        self.gateway = gateway ?? subnet.gateway
        self.mtu = mtu
        self.egressInterface = egressInterface.map(Self.validateInterfaceName)
        self.enableNAT = enableNAT
        self.log = logger ?? Logger(label: "com.apple.containerization.bridge")
    }

    /// Reject obviously-bogus interface names before they hit netlink or
    /// `iptables`. This is a defense-in-depth check; the kernel and
    /// `iptables` themselves will also reject pathological inputs, but doing
    /// it here surfaces the error in a callable Swift API rather than as a
    /// netlink rc or iptables exit. Asserts (rather than throws) — these
    /// constraints are static, so a violation is a programming error.
    private static func validateInterfaceName(_ name: String) -> String {
        // IFNAMSIZ on Linux is 16 (15 usable + NUL). iptables itself caps
        // at 15. Names with `/`, whitespace, or NUL are kernel-rejected.
        precondition(!name.isEmpty, "interface name must be non-empty")
        precondition(name.utf8.count <= 15, "interface name '\(name)' exceeds IFNAMSIZ-1 (15)")
        precondition(
            !name.contains(where: { $0.isWhitespace || $0 == "/" || $0 == "\0" || $0 == ":" }),
            "interface name '\(name)' contains invalid characters"
        )
        return name
    }

    /// Idempotent create.
    public func create() throws {
        try Self.ensureStateDirectory()
        let lock = try FileLock(path: Self.lockPath(for: name))
        try lock.withExclusive {
            try createLocked()
        }
    }

    /// Idempotent delete. No-op when the bridge does not exist.
    public func delete() throws {
        try Self.ensureStateDirectory()
        let lock = try FileLock(path: Self.lockPath(for: name))
        try lock.withExclusive {
            try deleteLocked()
        }
    }

    private func createLocked() throws {
        let session = NetlinkSession(socket: try DefaultNetlinkSocket(), log: log)
        let stateURL = URL(fileURLWithPath: Self.statePath(for: name))

        // Preserve `prevIpForward` across re-runs: a second NAT-enabled
        // create() call would otherwise read the value the FIRST run left
        // behind ("1") and clobber the original prior state, so delete()
        // couldn't restore.
        let priorState: BridgeState? = (try? Data(contentsOf: stateURL))
            .flatMap { try? BridgeState.decode($0) }

        // 1. Bridge link.
        do {
            try session.linkAddBridge(name: name)
            log.info("created bridge \(name)")
        } catch {
            // EEXIST is fine; treat any error as "maybe it exists" and probe.
            // `linkGet` throws ENODEV when the iface is absent (rather than
            // returning an empty array), so coalesce both shapes to "absent".
            let existing = (try? session.linkGet(interface: name)) ?? []
            if existing.isEmpty {
                throw ContainerizationError(
                    .internalError,
                    message: "linkAddBridge \(name) failed and bridge does not exist: \(error)"
                )
            }
            log.debug("bridge \(name) already exists")
        }

        // 2. Address (gateway/prefix) on the bridge.
        let cidr = try CIDRv4(gateway, prefix: subnet.prefix)
        do {
            try session.addressAdd(interface: name, ipv4Address: cidr)
        } catch {
            // EEXIST tolerated; netlink layer doesn't expose errno cleanly,
            // so log and continue. linkSet/up below will fail visibly if the
            // bridge state is actually broken.
            log.debug("addressAdd \(cidr) on \(name) returned \(error) (likely already set)")
        }

        // 3. Up + MTU.
        try session.linkSet(interface: name, up: true, mtu: mtu)

        // NAT is opt-in but sticky: once enabled by a previous create(),
        // subsequent create() calls without --enable-nat leave the existing
        // rules and ip_forward state in place. Otherwise `cctl run`
        // (defaults to NAT off) called after `cctl bridge create
        // --enable-nat` would silently disable the NAT the user explicitly
        // turned on. delete() always reverses whatever the state file
        // records.
        let effectiveNAT = enableNAT || (priorState?.natEnabled ?? false)
        guard effectiveNAT else {
            let state = BridgeState(natEnabled: false)
            try state.encode().write(to: stateURL)
            log.info("bridge \(name) ready (subnet \(subnet), NAT disabled)")
            return
        }

        // 4. ip_forward: read what's currently on the host, decide what to
        //    record. If we already have a NAT-enabled state file from a prior
        //    create(), keep its `prevIpForward` (it's the *original* prior
        //    value); otherwise record what we just read.
        let currentIpForward = (try? Self.readSysctl("net/ipv4/ip_forward")) ?? "0"
        let prevIpForward = (priorState?.natEnabled == true ? priorState?.prevIpForward : nil) ?? currentIpForward
        if currentIpForward != "1" {
            try Self.writeSysctl("net/ipv4/ip_forward", value: "1")
        }

        // 5. Egress iface — explicit override or auto-detect.
        let egress: String
        if let explicit = egressInterface {
            egress = explicit
        } else if let detected = HostDefaultRoute.currentEgress() {
            egress = detected
        } else {
            throw ContainerizationError(
                .invalidArgument,
                message: "no default route on host; pass egressInterface explicitly"
            )
        }

        // 6. Record state BEFORE iptables. If a later iptables -A fails,
        //    delete() still has authority to clean up partial rules; if we
        //    deferred the write until after, a mid-failure would orphan rules
        //    with no record.
        let state = BridgeState(
            natEnabled: true,
            prevIpForward: prevIpForward,
            egressInterface: egress
        )
        try state.encode().write(to: stateURL)

        // 7. iptables rules — idempotent. The FORWARD rule is scoped to
        //    `-i <bridge> -o <egress>` so the host doesn't become an
        //    unrestricted router for guest traffic across every host iface
        //    (e.g. a VPN or a sibling bridge).
        try IptablesRules.ensure(
            table: "nat",
            args: [
                "POSTROUTING", "-s", subnet.description, "!", "-o", name, "-j", "MASQUERADE",
            ])
        try IptablesRules.ensure(args: [
            "FORWARD", "-i", name, "-o", egress, "-j", "ACCEPT",
        ])
        try IptablesRules.ensure(args: [
            "FORWARD", "-i", egress, "-o", name, "-m", "conntrack", "--ctstate", "RELATED,ESTABLISHED", "-j", "ACCEPT",
        ])

        log.info("bridge \(name) ready (subnet \(subnet), egress \(egress), NAT enabled)")
    }

    private func deleteLocked() throws {
        let stateURL = URL(fileURLWithPath: Self.statePath(for: name))
        let state: BridgeState? = (try? Data(contentsOf: stateURL))
            .flatMap { try? BridgeState.decode($0) }

        // 1. iptables — only if a prior create() with NAT enabled left state
        //    we own. The rules are keyed off subnet, bridge name, and the
        //    recorded egress iface, so removal is precise even when the
        //    host has rules from other tools.
        if let state, state.natEnabled, let egress = state.egressInterface {
            log.debug("removing iptables rules for bridge \(name) (egress \(egress))")
            IptablesRules.remove(
                table: "nat",
                args: [
                    "POSTROUTING", "-s", subnet.description, "!", "-o", name, "-j", "MASQUERADE",
                ])
            IptablesRules.remove(args: [
                "FORWARD", "-i", name, "-o", egress, "-j", "ACCEPT",
            ])
            IptablesRules.remove(args: [
                "FORWARD", "-i", egress, "-o", name, "-m", "conntrack", "--ctstate", "RELATED,ESTABLISHED", "-j", "ACCEPT",
            ])
        }

        // 2. Bridge link.
        let session = NetlinkSession(socket: try DefaultNetlinkSocket(), log: log)
        // Refuse to delete anything that isn't actually a bridge — the
        // kernel exposes `/sys/class/net/<iface>/bridge` only for links of
        // kind=bridge, so its presence is an authoritative kind check
        // without parsing IFLA_LINKINFO. This guards against `cctl bridge
        // delete --name eth0` (or docker0, etc.) taking down host links.
        let sysfsBridge = "/sys/class/net/\(name)/bridge"
        let isBridge = FileManager.default.fileExists(atPath: sysfsBridge)
        let exists = !((try? session.linkGet(interface: name)) ?? []).isEmpty
        if exists && !isBridge {
            throw ContainerizationError(
                .invalidArgument,
                message: "refusing to delete \(name): exists but is not a bridge interface"
            )
        }
        do {
            try session.linkDel(name: name)
            log.info("deleted bridge \(name)")
        } catch {
            // ENODEV-like: nothing to do.
            log.debug("linkDel \(name) returned \(error) (likely already absent)")
        }

        // 3. Restore ip_forward only if this bridge's create()-with-NAT set
        //    it from 0 AND no other containerization bridge still has NAT
        //    enabled. ip_forward is a single global sysctl shared by every
        //    bridge, so it must be reference-counted against the on-disk state
        //    files rather than blindly reset — otherwise tearing down one NAT
        //    bridge would disable forwarding for a sibling that still needs it.
        //
        //    (Torn down in the order that removes the original flipper first —
        //    or two NAT bridges torn down concurrently — may leave ip_forward=1
        //    after the last bridge is gone. That's the safe direction:
        //    forwarding with no bridge or iptables rules attached is inert, and
        //    a reboot clears it. Erroneously forcing it to 0 under a live NAT
        //    bridge is the failure this guards against.)
        let otherNAT = Self.otherNATEnabledBridgesExist(excluding: name)
        if state?.natEnabled == true, state?.prevIpForward == "0", !otherNAT {
            try? Self.writeSysctl("net/ipv4/ip_forward", value: "0")
        }

        // 4. Remove state file.
        try? FileManager.default.removeItem(at: stateURL)
    }

    // MARK: - Paths / sysctl helpers

    private static let stateDir = "/run/containerization"

    private static func statePath(for name: String) -> String {
        "\(stateDir)/bridge-\(name).state"
    }

    private static func lockPath(for name: String) -> String {
        "\(stateDir)/bridge-\(name).lock"
    }

    private static func ensureStateDirectory() throws {
        try FileManager.default.createDirectory(
            atPath: stateDir,
            withIntermediateDirectories: true,
            attributes: [.posixPermissions: 0o755]
        )
    }

    private static func readSysctl(_ path: String) throws -> String {
        let url = URL(fileURLWithPath: "/proc/sys/\(path)")
        return try String(contentsOf: url, encoding: .utf8)
            .trimmingCharacters(in: .whitespacesAndNewlines)
    }

    private static func writeSysctl(_ path: String, value: String) throws {
        let url = URL(fileURLWithPath: "/proc/sys/\(path)")
        try Data((value + "\n").utf8).write(to: url)
    }

    /// Whether any *other* containerization bridge still has NAT enabled,
    /// determined by scanning the `bridge-*.state` files under `stateDir`.
    /// Used by `delete()` to reference-count the shared global `ip_forward`
    /// sysctl so tearing down one NAT bridge doesn't disable forwarding for
    /// its siblings. `excluding` is this bridge's name — its own (still
    /// present) state file is skipped since `delete()` removes it afterward.
    private static func otherNATEnabledBridgesExist(excluding name: String) -> Bool {
        let selfFile = "bridge-\(name).state"
        let entries = (try? FileManager.default.contentsOfDirectory(atPath: stateDir)) ?? []
        for entry in entries {
            guard entry.hasPrefix("bridge-"), entry.hasSuffix(".state"), entry != selfFile else {
                continue
            }
            let url = URL(fileURLWithPath: "\(stateDir)/\(entry)")
            guard
                let data = try? Data(contentsOf: url),
                let state = try? BridgeState.decode(data)
            else {
                continue
            }
            if state.natEnabled {
                return true
            }
        }
        return false
    }
}

/// `flock(2)` wrapper. Held for the duration of a closure.
struct FileLock {
    let fd: Int32

    init(path: String) throws {
        let f = open(path, O_RDWR | O_CREAT | O_CLOEXEC, 0o600)
        guard f >= 0 else {
            throw ContainerizationError(
                .internalError,
                message: "open \(path) failed: errno=\(errno)"
            )
        }
        self.fd = f
    }

    func withExclusive<T>(_ body: () throws -> T) throws -> T {
        guard flock(fd, LOCK_EX) == 0 else {
            close(fd)
            throw ContainerizationError(
                .internalError,
                message: "flock LOCK_EX failed: errno=\(errno)"
            )
        }
        defer {
            _ = flock(fd, LOCK_UN)
            close(fd)
        }
        return try body()
    }
}
#endif
