package lxc

import (
	"context"
	"encoding/binary"
	"fmt"
	"math/big"
	"net/netip"
	"os"
	"os/exec"
	"path/filepath"
	"sort"
	"strconv"
	"strings"
	"time"

	"clicd/internal/config"
)

const ipv6GatewayLinkLocal = "fe80::1"
const MaxPublicIPAssignments = 64

type IPv6PrefixInfo struct {
	Interface string `json:"interface"`
	Address   string `json:"address"`
	Prefix    string `json:"prefix"`
	PrefixLen int    `json:"prefix_len"`
	Gateway   string `json:"gateway"`
	IsTunnel  bool   `json:"is_tunnel"`
	Source    string `json:"source"`
}

type PublicIPInfo struct {
	Address    string `json:"address"`
	Interface  string `json:"interface"`
	Prefix     string `json:"prefix"`
	PrefixLen  int    `json:"prefix_len,omitempty"`
	SubnetMask string `json:"subnet_mask,omitempty"`
	Gateway    string `json:"gateway,omitempty"`
	IsTunnel   bool   `json:"is_tunnel"`
	Source     string `json:"source"`
}

type PublicIPv4ScanResult struct {
	PublicIPInfo
	Status string `json:"status"`
	Usable bool   `json:"usable"`
	Reason string `json:"reason"`
}

type IPv6Status struct {
	Available bool             `json:"available"`
	Reachable bool             `json:"reachable"`
	Reason    string           `json:"reason"`
	Prefixes  []IPv6PrefixInfo `json:"prefixes"`
}

func (m *Manager) DetectIPv6Status() IPv6Status {
	status := IPv6Status{}
	prefixes := DetectPublicIPv6Prefixes()
	status.Prefixes = prefixes
	if len(prefixes) == 0 {
		status.Reason = "no usable public IPv6 prefix found; /128 single-address IPv6 is not assignable"
		return status
	}
	status.Reachable = ipv6ConnectivityOK()
	if !status.Reachable {
		status.Reason = "host has an IPv6 prefix, but outbound IPv6 connectivity test failed"
		return status
	}
	status.Available = true
	status.Reason = "usable public IPv6 prefix detected"
	return status
}

func DetectPublicIPv6Prefixes() []IPv6PrefixInfo {
	return detectPublicIPv6Prefixes(detectIPv6DefaultRoutes())
}

func DetectHostPublicIPv6Prefixes() []IPv6PrefixInfo {
	return detectPublicIPv6Prefixes(detectIPv6DefaultRoutes())
}

func ConfiguredPublicIPv6Prefixes() []IPv6PrefixInfo {
	if config.AppConfig == nil || len(config.AppConfig.PublicIPv6Prefixes) == 0 {
		return nil
	}
	detected := detectPublicIPv6Prefixes(detectIPv6DefaultRoutes())
	defaultIface := ""
	defaultGateway := ""
	for _, item := range detected {
		if defaultIface == "" {
			defaultIface = item.Interface
		}
		if defaultGateway == "" {
			defaultGateway = item.Gateway
		}
	}
	result := make([]IPv6PrefixInfo, 0, len(config.AppConfig.PublicIPv6Prefixes))
	for _, item := range config.AppConfig.PublicIPv6Prefixes {
		address := strings.TrimSpace(item.Address)
		prefixText := strings.TrimSpace(item.Prefix)
		if prefixText == "" && address != "" && item.PrefixLen > 0 {
			prefixText = address + "/" + strconv.Itoa(item.PrefixLen)
		}
		prefix, err := netip.ParsePrefix(prefixText)
		if err != nil || !prefix.Addr().Is6() {
			continue
		}
		iface := strings.TrimSpace(item.Interface)
		if iface == "" {
			iface = defaultIface
		}
		gateway := strings.TrimSpace(item.Gateway)
		if gateway == "" {
			gateway = defaultGateway
		}
		addr := strings.TrimSpace(item.Address)
		if addr == "" {
			addr = prefix.Addr().String()
		}
		result = append(result, IPv6PrefixInfo{
			Interface: iface,
			Address:   addr,
			Prefix:    prefix.Masked().String(),
			PrefixLen: prefix.Bits(),
			Gateway:   gateway,
			IsTunnel:  isTunnelLikeInterface(iface),
			Source:    "manual",
		})
	}
	sort.SliceStable(result, func(i, j int) bool {
		return result[i].Prefix < result[j].Prefix
	})
	return result
}

func DetectPublicIPv4() PublicIPInfo {
	addresses := detectPublicIPv4LocalAddresses()
	if len(addresses) == 0 {
		return PublicIPInfo{}
	}
	return addresses[0]
}

func DetectPublicIPv4Candidates() []PublicIPInfo {
	return ConfiguredPublicIPv4Pool()
}

func DetectFreePublicIPv4Candidates(skipContainerID int) []PublicIPInfo {
	used := assignedPublicIPv4Map(skipContainerID)
	candidates := DetectPublicIPv4Candidates()
	result := make([]PublicIPInfo, 0, len(candidates))
	for _, item := range candidates {
		if item.Address == "" || used[item.Address] {
			continue
		}
		result = append(result, item)
	}
	return result
}

func ConfiguredPublicIPv4Pool() []PublicIPInfo {
	if config.AppConfig == nil {
		return nil
	}
	localAddrs := detectPublicIPv4LocalAddresses()
	primaryHostIP := ""
	defaultIface := ""
	defaultPrefixLen := 32
	defaultRoutes := detectIPv4DefaultRoutes()
	gateways := ipv4GatewaysByInterface(defaultRoutes)
	if len(localAddrs) > 0 {
		primaryHostIP = localAddrs[0].Address
		defaultIface = localAddrs[0].Interface
		if localAddrs[0].PrefixLen > 0 {
			defaultPrefixLen = localAddrs[0].PrefixLen
		}
	}

	type candidate struct {
		info  PublicIPInfo
		score int
	}
	candidatesByAddr := map[string]candidate{}
	addCandidate := func(info PublicIPInfo, score int) {
		if info.Address == "" || info.Address == primaryHostIP || (info.Gateway != "" && info.Address == info.Gateway) {
			return
		}
		if existing, ok := candidatesByAddr[info.Address]; ok && existing.score >= score {
			return
		}
		candidatesByAddr[info.Address] = candidate{info: info, score: score}
	}

	defaultIfaces := map[string]bool{}
	for _, route := range defaultRoutes {
		defaultIfaces[route.Interface] = true
	}
	for _, item := range config.AppConfig.PublicIPv4Pool {
		address := strings.TrimSpace(item.Address)
		if address == "" {
			continue
		}
		addr, err := netip.ParseAddr(address)
		if err != nil || !addr.Is4() {
			continue
		}
		address = addr.String()
		iface := strings.TrimSpace(item.Interface)
		gateway := strings.TrimSpace(item.Gateway)
		if iface == "" && gateway != "" {
			iface = ipv4InterfaceForGateway(gateway, defaultRoutes)
		}
		if iface == "" {
			iface = defaultIface
		}
		if gateway == "" && iface != "" {
			gateway = gateways[iface]
		}
		prefixLen := item.PrefixLen
		if prefixLen <= 0 || prefixLen > 32 {
			if gatewayAddr, err := netip.ParseAddr(gateway); err == nil && gatewayAddr.Is4() {
				prefixLen = inferPublicIPv4PrefixLen(addr, gatewayAddr, iface, localAddrs)
			}
			if prefixLen <= 0 || prefixLen > 32 {
				prefixLen = defaultPrefixLen
			}
		}
		score := publicInterfaceScore(iface, defaultIfaces)
		addCandidate(PublicIPInfo{
			Address:    address,
			Interface:  iface,
			Prefix:     ipv4PrefixString(addr, prefixLen),
			PrefixLen:  prefixLen,
			SubnetMask: ipv4SubnetMask(prefixLen),
			Gateway:    gateway,
			IsTunnel:   isTunnelLikeInterface(iface),
			Source:     "manual",
		}, score+50)
	}

	candidates := make([]candidate, 0, len(candidatesByAddr))
	for _, item := range candidatesByAddr {
		candidates = append(candidates, item)
	}
	sort.SliceStable(candidates, func(i, j int) bool {
		if candidates[i].score == candidates[j].score {
			return compareIPv4Strings(candidates[i].info.Address, candidates[j].info.Address) < 0
		}
		return candidates[i].score > candidates[j].score
	})
	result := make([]PublicIPInfo, 0, len(candidates))
	for _, c := range candidates {
		result = append(result, c.info)
	}
	return result
}

func NormalizePublicIPv4Pool(items []config.PublicIPv4Assignment) ([]config.PublicIPv4Assignment, error) {
	localAddrs := detectPublicIPv4LocalAddresses()
	primaryHostIP := ""
	defaultIface := ""
	defaultRoutes := detectIPv4DefaultRoutes()
	gateways := ipv4GatewaysByInterface(defaultRoutes)
	localMap := map[string]bool{}
	if len(localAddrs) > 0 {
		primaryHostIP = localAddrs[0].Address
		defaultIface = localAddrs[0].Interface
	}
	for _, local := range localAddrs {
		localMap[local.Address] = true
	}
	assigned := assignedPublicIPv4Map(0)

	seen := map[string]bool{}
	result := make([]config.PublicIPv4Assignment, 0, len(items))
	for _, item := range items {
		raw := strings.TrimSpace(item.Address)
		if raw == "" {
			continue
		}
		gateway := strings.TrimSpace(item.Gateway)
		if gateway == "" && item.Interface != "" {
			gateway = gateways[strings.TrimSpace(item.Interface)]
		}
		if gateway == "" {
			return nil, fmt.Errorf("gateway is required for IPv4 %s", raw)
		}
		gatewayAddr, err := netip.ParseAddr(gateway)
		if err != nil || !gatewayAddr.Is4() {
			return nil, fmt.Errorf("gateway %s is not a valid IPv4 address", gateway)
		}
		gateway = gatewayAddr.String()

		iface := strings.TrimSpace(item.Interface)
		if iface == "" {
			iface = ipv4InterfaceForGateway(gateway, defaultRoutes)
		}
		if iface == "" {
			iface = defaultIface
		}
		if iface == "" {
			return nil, fmt.Errorf("interface is required for IPv4 %s", raw)
		}

		addresses, prefixLen, singleInput, err := expandPublicIPv4Input(raw, item.PrefixLen, gatewayAddr, iface, localAddrs)
		if err != nil {
			return nil, err
		}
		addedFromInput := 0
		skippedReasons := []string{}
		for _, addr := range addresses {
			if !addr.Is4() || !isPublicIPv4(addr) {
				if singleInput {
					return nil, fmt.Errorf("IPv4 %s is not a valid public IPv4 address", addr.String())
				}
				continue
			}
			address := addr.String()
			switch {
			case address == primaryHostIP:
				if singleInput {
					return nil, fmt.Errorf("IPv4 %s is the host primary IPv4 and cannot be allocated", address)
				}
				skippedReasons = append(skippedReasons, address+" is the host primary IPv4")
				continue
			case address == gateway:
				if singleInput {
					return nil, fmt.Errorf("IPv4 %s is the gateway and cannot be allocated", address)
				}
				skippedReasons = append(skippedReasons, address+" is the gateway")
				continue
			case seen[address]:
				continue
			case ipv4AddressResponds(iface, address) && !localMap[address] && !assigned[address]:
				if singleInput {
					return nil, fmt.Errorf("IPv4 %s responds on the network and cannot be allocated", address)
				}
				skippedReasons = append(skippedReasons, address+" responds on the network")
				continue
			}
			seen[address] = true
			result = append(result, config.PublicIPv4Assignment{
				Address:   address,
				Interface: iface,
				PrefixLen: prefixLen,
				Gateway:   gateway,
			})
			addedFromInput++
		}
		if singleInput && addedFromInput == 0 {
			if len(skippedReasons) > 0 {
				return nil, fmt.Errorf("IPv4 %s is not allocatable: %s", raw, strings.Join(skippedReasons, "; "))
			}
			return nil, fmt.Errorf("IPv4 %s is not allocatable", raw)
		}
	}
	if len(result) == 0 && len(items) > 0 {
		return nil, fmt.Errorf("no allocatable public IPv4 address found in the submitted pool")
	}
	sort.SliceStable(result, func(i, j int) bool {
		return compareIPv4Strings(result[i].Address, result[j].Address) < 0
	})
	return result, nil
}

func expandPublicIPv4Input(raw string, requestedPrefixLen int, gateway netip.Addr, iface string, localAddrs []PublicIPInfo) ([]netip.Addr, int, bool, error) {
	if strings.Contains(raw, "/") {
		prefix, err := netip.ParsePrefix(raw)
		if err != nil || !prefix.Addr().Is4() {
			return nil, 0, false, fmt.Errorf("invalid IPv4 segment: %s", raw)
		}
		prefix = prefix.Masked()
		if prefix.Bits() < 24 {
			return nil, 0, false, fmt.Errorf("IPv4 segment %s is too large; use /24 or smaller", prefix.String())
		}
		return ipv4HostsInPrefix(prefix), prefix.Bits(), prefix.Bits() == 32, nil
	}

	addr, err := netip.ParseAddr(raw)
	if err != nil || !addr.Is4() {
		return nil, 0, true, fmt.Errorf("IPv4 %s is not a valid public IPv4 address", raw)
	}
	prefixLen := requestedPrefixLen
	if prefixLen <= 0 || prefixLen > 32 {
		prefixLen = inferPublicIPv4PrefixLen(addr, gateway, iface, localAddrs)
	}
	if prefixLen <= 0 || prefixLen > 32 {
		prefixLen = 32
	}
	return []netip.Addr{addr}, prefixLen, true, nil
}

func ipv4GatewaysByInterface(routes []routeInfo) map[string]string {
	gateways := map[string]string{}
	for _, route := range routes {
		if route.Interface != "" && route.Gateway != "" && gateways[route.Interface] == "" {
			gateways[route.Interface] = route.Gateway
		}
	}
	return gateways
}

func ipv4InterfaceForGateway(gateway string, defaultRoutes []routeInfo) string {
	gateway = strings.TrimSpace(gateway)
	if gateway == "" {
		return ""
	}
	for _, route := range defaultRoutes {
		if route.Gateway == gateway && route.Interface != "" {
			return route.Interface
		}
	}
	out, err := exec.Command("ip", "-4", "route", "get", gateway).Output()
	if err != nil {
		return ""
	}
	fields := strings.Fields(string(out))
	for i := 0; i < len(fields)-1; i++ {
		if fields[i] == "dev" {
			return normalizeIface(fields[i+1])
		}
	}
	return ""
}

func assignedPublicIPv4Map(skipContainerID int) map[string]bool {
	assigned := map[string]bool{}
	if config.AppConfig == nil {
		return assigned
	}
	for _, c := range config.AppConfig.Containers {
		if skipContainerID > 0 && c.ID == skipContainerID {
			continue
		}
		for _, item := range c.PublicIPv4s {
			if address := strings.TrimSpace(item.Address); address != "" {
				assigned[address] = true
			}
		}
	}
	return assigned
}

func inferPublicIPv4PrefixLen(addr netip.Addr, gateway netip.Addr, iface string, localAddrs []PublicIPInfo) int {
	best := 0
	consider := func(prefixText string, prefixIface string) {
		if iface != "" && prefixIface != "" && iface != prefixIface {
			return
		}
		prefix, err := netip.ParsePrefix(prefixText)
		if err != nil || !prefix.Addr().Is4() || !prefix.Contains(addr) {
			return
		}
		if gateway.IsValid() && gateway.Is4() && !prefix.Contains(gateway) {
			return
		}
		if bits := prefix.Bits(); bits > best {
			best = bits
		}
	}
	for _, local := range localAddrs {
		if local.Prefix != "" {
			consider(local.Prefix, local.Interface)
			continue
		}
		if local.Address != "" && local.PrefixLen > 0 {
			consider(local.Address+"/"+strconv.Itoa(local.PrefixLen), local.Interface)
		}
	}

	out, err := exec.Command("ip", "-4", "route", "show").Output()
	if err != nil {
		return best
	}
	for _, line := range strings.Split(string(out), "\n") {
		fields := strings.Fields(line)
		if len(fields) == 0 || fields[0] == "default" {
			continue
		}
		if !strings.Contains(fields[0], "/") {
			continue
		}
		routeIface := ""
		for i := 0; i < len(fields)-1; i++ {
			if fields[i] == "dev" {
				routeIface = normalizeIface(fields[i+1])
				break
			}
		}
		consider(fields[0], routeIface)
	}
	return best
}

func ipv4PrefixString(addr netip.Addr, prefixLen int) string {
	if prefixLen <= 0 || prefixLen > 32 {
		prefixLen = 32
	}
	prefix, err := netip.ParsePrefix(addr.String() + "/" + strconv.Itoa(prefixLen))
	if err != nil {
		return addr.String() + "/" + strconv.Itoa(prefixLen)
	}
	return prefix.Masked().String()
}

func ipv4SubnetMask(prefixLen int) string {
	if prefixLen < 0 || prefixLen > 32 {
		return ""
	}
	var mask uint32
	if prefixLen > 0 {
		mask = ^uint32(0) << uint(32-prefixLen)
	}
	return uint32ToIPv4(mask).String()
}

func NormalizePublicIPv6Prefixes(items []config.PublicIPv6Prefix) ([]config.PublicIPv6Prefix, error) {
	detected := detectPublicIPv6Prefixes(detectIPv6DefaultRoutes())
	defaultIface := ""
	defaultGateway := ""
	for _, item := range detected {
		if defaultIface == "" {
			defaultIface = item.Interface
		}
		if defaultGateway == "" {
			defaultGateway = item.Gateway
		}
	}
	seen := map[string]bool{}
	result := make([]config.PublicIPv6Prefix, 0, len(items))
	for _, item := range items {
		raw := strings.TrimSpace(item.Prefix)
		if raw == "" {
			raw = strings.TrimSpace(item.Address)
			if raw != "" && !strings.Contains(raw, "/") && item.PrefixLen > 0 {
				raw += "/" + strconv.Itoa(item.PrefixLen)
			}
		}
		if raw == "" {
			continue
		}
		prefix, err := netip.ParsePrefix(raw)
		if err != nil || !prefix.Addr().Is6() {
			return nil, fmt.Errorf("invalid IPv6 prefix: %s", raw)
		}
		if prefix.Bits() > 120 {
			return nil, fmt.Errorf("IPv6 prefix %s is too small for allocation", prefix.String())
		}
		if !isPublicIPv6(prefix.Addr()) {
			return nil, fmt.Errorf("IPv6 prefix %s is not public", prefix.String())
		}
		key := prefix.Masked().String()
		if seen[key] {
			continue
		}
		iface := strings.TrimSpace(item.Interface)
		if iface == "" {
			iface = defaultIface
		}
		if iface == "" {
			return nil, fmt.Errorf("interface is required for IPv6 prefix %s", key)
		}
		gateway := strings.TrimSpace(item.Gateway)
		if gateway == "" {
			gateway = defaultGateway
		}
		seen[key] = true
		result = append(result, config.PublicIPv6Prefix{
			Address:   prefix.Addr().String(),
			Prefix:    key,
			PrefixLen: prefix.Bits(),
			Interface: iface,
			Gateway:   gateway,
		})
	}
	sort.SliceStable(result, func(i, j int) bool {
		return result[i].Prefix < result[j].Prefix
	})
	return result, nil
}

func ScanPublicIPv4Segment(cidr string, iface string, gateway string, verify bool, limit int) ([]PublicIPv4ScanResult, error) {
	if limit <= 0 || limit > 256 {
		limit = 256
	}
	cidr = strings.TrimSpace(cidr)
	if cidr == "" {
		if host := DetectPublicIPv4(); host.Prefix != "" {
			cidr = host.Prefix
		}
	}
	prefix, err := netip.ParsePrefix(cidr)
	if err != nil || !prefix.Addr().Is4() {
		return nil, fmt.Errorf("invalid IPv4 segment: %s", cidr)
	}
	prefix = prefix.Masked()
	bits := prefix.Bits()
	if bits < 24 {
		return nil, fmt.Errorf("IPv4 segment %s is too large; use /24 or smaller", prefix.String())
	}

	localAddrs := detectPublicIPv4LocalAddresses()
	primaryHostIP := ""
	defaultIface := strings.TrimSpace(iface)
	defaultPrefixLen := bits
	defaultRoutes := detectIPv4DefaultRoutes()
	gateway = strings.TrimSpace(gateway)
	if gateway == "" && defaultIface != "" {
		gateway = ipv4GatewaysByInterface(defaultRoutes)[defaultIface]
	}
	if gateway != "" {
		gatewayAddr, err := netip.ParseAddr(gateway)
		if err != nil || !gatewayAddr.Is4() {
			return nil, fmt.Errorf("gateway %s is not a valid IPv4 address", gateway)
		}
		gateway = gatewayAddr.String()
	}
	if defaultIface == "" && gateway != "" {
		defaultIface = ipv4InterfaceForGateway(gateway, defaultRoutes)
	}
	localMap := map[string]bool{}
	for _, local := range localAddrs {
		if primaryHostIP == "" {
			primaryHostIP = local.Address
		}
		if defaultIface == "" {
			defaultIface = local.Interface
		}
		localMap[local.Address] = true
	}
	if defaultIface == "" {
		return nil, fmt.Errorf("interface is required")
	}
	if gateway == "" {
		return nil, fmt.Errorf("gateway is required")
	}
	assigned := map[string]bool{}
	for _, c := range config.AppConfig.Containers {
		for _, item := range c.PublicIPv4s {
			if item.Address != "" {
				assigned[item.Address] = true
			}
		}
	}
	inPool := map[string]bool{}
	for _, item := range config.AppConfig.PublicIPv4Pool {
		if item.Address != "" {
			inPool[item.Address] = true
		}
	}

	addresses := ipv4HostsInPrefix(prefix)
	if len(addresses) > limit {
		return nil, fmt.Errorf("IPv4 segment %s has %d hosts; limit is %d", prefix.String(), len(addresses), limit)
	}

	results := make([]PublicIPv4ScanResult, 0, len(addresses))
	for _, addr := range addresses {
		address := addr.String()
		info := PublicIPInfo{
			Address:    address,
			Interface:  defaultIface,
			Prefix:     prefix.String(),
			PrefixLen:  defaultPrefixLen,
			SubnetMask: ipv4SubnetMask(defaultPrefixLen),
			Gateway:    gateway,
			IsTunnel:   isTunnelLikeInterface(defaultIface),
			Source:     "scan",
		}
		result := PublicIPv4ScanResult{PublicIPInfo: info, Status: "unknown", Reason: "not checked"}
		switch {
		case address == primaryHostIP:
			result.Status = "host"
			result.Reason = "host primary IPv4"
		case address == gateway:
			result.Status = "gateway"
			result.Reason = "default gateway"
		case assigned[address]:
			result.Status = "assigned"
			result.Usable = true
			result.Reason = "already assigned to a container"
		case inPool[address]:
			result.Status = "pool"
			result.Usable = true
			result.Reason = "already in allocation pool"
		case localMap[address]:
			result.Status = "configured"
			result.Usable = true
			result.Reason = "already configured on host"
		case ipv4AddressResponds(defaultIface, address):
			result.Status = "in_use"
			result.Reason = "address responds on the network"
		case verify:
			if ok, reason := verifyIPv4SourceUsable(defaultIface, address); ok {
				result.Status = "available"
				result.Usable = true
				result.Reason = reason
			} else {
				result.Status = "unknown"
				result.Reason = reason
			}
		default:
			result.Status = "available"
			result.Usable = true
			result.Reason = "no duplicate response detected; source routing not verified"
		}
		results = append(results, result)
	}
	return results, nil
}

func ipv4HostsInPrefix(prefix netip.Prefix) []netip.Addr {
	bits := prefix.Bits()
	base := ipv4ToUint32(prefix.Masked().Addr())
	count := uint64(1) << uint(32-bits)
	if bits == 32 {
		return []netip.Addr{uint32ToIPv4(base)}
	}
	start := uint64(base)
	end := start + count - 1
	if bits <= 30 {
		start++
		end--
	}
	result := make([]netip.Addr, 0, count)
	for value := start; value <= end; value++ {
		result = append(result, uint32ToIPv4(uint32(value)))
	}
	return result
}

func ipv4AddressResponds(iface, address string) bool {
	if commandExists("arping") {
		if exec.Command("arping", "-D", "-I", iface, "-c", "2", "-w", "3", address).Run() != nil {
			return true
		}
		return false
	}
	return exec.Command("ping", "-4", "-I", iface, "-c", "1", "-W", "1", address).Run() == nil
}

func verifyIPv4SourceUsable(iface, address string) (bool, string) {
	cidr := address + "/32"
	added := false
	if exec.Command("ip", "-4", "addr", "show", "dev", iface, "to", cidr).Run() != nil {
		output, err := exec.Command("ip", "-4", "addr", "add", cidr, "dev", iface, "label", iface+":clicdscan").CombinedOutput()
		if err != nil {
			return false, "failed to temporarily bind address: " + strings.TrimSpace(string(output))
		}
		added = true
	}
	if added {
		defer exec.Command("ip", "-4", "addr", "del", cidr, "dev", iface).Run()
	}
	if commandExists("curl") {
		for _, target := range []string{"https://api.ipify.org", "https://ifconfig.me/ip", "http://ifconfig.me/ip"} {
			ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
			out, err := exec.CommandContext(ctx, "curl", "-4", "-sS", "--interface", address, "--max-time", "4", target).Output()
			cancel()
			if err == nil && strings.TrimSpace(string(out)) == address {
				return true, "source IPv4 verified by external check"
			}
		}
	}
	for _, target := range []string{"1.1.1.1", "8.8.8.8"} {
		if exec.Command("ping", "-4", "-I", address, "-c", "1", "-W", "2", target).Run() == nil {
			return true, "source IPv4 can reach external network"
		}
	}
	return false, "no duplicate response, but source IPv4 verification failed"
}

func commandExists(name string) bool {
	_, err := exec.LookPath(name)
	return err == nil
}

func detectPublicIPv4LocalAddresses() []PublicIPInfo {
	out, err := exec.Command("ip", "-4", "-o", "addr", "show", "scope", "global").Output()
	if err != nil {
		return nil
	}
	defaultRoutes := detectIPv4DefaultRoutes()
	defaultIfaces := map[string]bool{}
	for _, route := range defaultRoutes {
		defaultIfaces[route.Interface] = true
	}
	type candidate struct {
		info  PublicIPInfo
		score int
	}
	var candidates []candidate
	seen := map[string]bool{}
	for _, line := range strings.Split(string(out), "\n") {
		fields := strings.Fields(line)
		if len(fields) < 4 || fields[2] != "inet" {
			continue
		}
		iface := normalizeIface(fields[1])
		if isContainerLikeInterface(iface) {
			continue
		}
		prefix, err := netip.ParsePrefix(fields[3])
		if err != nil || !prefix.Addr().Is4() || !isPublicIPv4(prefix.Addr()) {
			continue
		}
		key := iface + "|" + prefix.Addr().String()
		if seen[key] {
			continue
		}
		seen[key] = true
		score := publicInterfaceScore(iface, defaultIfaces)
		candidates = append(candidates, candidate{
			info: PublicIPInfo{
				Address:    prefix.Addr().String(),
				Interface:  iface,
				Prefix:     prefix.Masked().String(),
				PrefixLen:  prefix.Bits(),
				SubnetMask: ipv4SubnetMask(prefix.Bits()),
				Gateway:    ipv4GatewayForInterface(defaultRoutes, iface),
				IsTunnel:   isTunnelLikeInterface(iface),
				Source:     "local",
			},
			score: score,
		})
	}
	sort.SliceStable(candidates, func(i, j int) bool {
		return candidates[i].score > candidates[j].score
	})
	result := make([]PublicIPInfo, 0, len(candidates))
	for _, c := range candidates {
		result = append(result, c.info)
	}
	return result
}

func AllocatePublicIPv4Assignments(id int, requested []string, count int, auto bool) ([]config.PublicIPv4Assignment, error) {
	var err error
	count, err = NormalizePublicIPAllocationCount(requested, count)
	if err != nil {
		return nil, err
	}
	candidates := DetectPublicIPv4Candidates()
	if len(candidates) == 0 {
		if len(requested) > 0 || auto {
			return nil, fmt.Errorf("public IPv4 allocation is unavailable: no usable public IPv4 address found")
		}
		return nil, nil
	}
	byAddress := map[string]PublicIPInfo{}
	for _, item := range candidates {
		byAddress[item.Address] = item
	}

	used := map[string]bool{}
	for _, c := range config.AppConfig.Containers {
		if c.ID == id {
			continue
		}
		for _, item := range c.PublicIPv4s {
			if item.Address != "" {
				used[item.Address] = true
			}
		}
	}

	result := []config.PublicIPv4Assignment{}
	selected := map[string]bool{}
	for _, raw := range requested {
		raw = strings.TrimSpace(raw)
		if raw == "" || selected[raw] {
			continue
		}
		addr, err := netip.ParseAddr(raw)
		if err != nil || !addr.Is4() {
			return nil, fmt.Errorf("requested IPv4 %s is not valid", raw)
		}
		raw = addr.String()
		if selected[raw] {
			continue
		}
		info, ok := byAddress[raw]
		if !ok {
			return nil, fmt.Errorf("requested IPv4 %s is not an allocatable public IPv4", raw)
		}
		if used[raw] {
			return nil, fmt.Errorf("requested IPv4 %s is already assigned", raw)
		}
		selected[raw] = true
		used[raw] = true
		result = append(result, config.PublicIPv4Assignment{Address: info.Address, Interface: info.Interface, PrefixLen: info.PrefixLen, Gateway: info.Gateway})
	}

	if len(result) >= count || !auto {
		return result, nil
	}
	for _, info := range candidates {
		if used[info.Address] || selected[info.Address] {
			continue
		}
		used[info.Address] = true
		selected[info.Address] = true
		result = append(result, config.PublicIPv4Assignment{Address: info.Address, Interface: info.Interface, PrefixLen: info.PrefixLen, Gateway: info.Gateway})
		if len(result) >= count {
			return result, nil
		}
	}
	if len(result) == 0 {
		return nil, fmt.Errorf("no free public IPv4 address is available")
	}
	if len(result) < count {
		return nil, fmt.Errorf("only %d free public IPv4 address(es) are available; %d requested", len(result), count)
	}
	return result, nil
}

func NormalizePublicIPAllocationCount(requested []string, count int) (int, error) {
	if len(requested) > MaxPublicIPAssignments {
		return 0, fmt.Errorf("IP address count cannot exceed %d", MaxPublicIPAssignments)
	}
	if count <= 0 {
		count = 1
	}
	if len(requested) > count {
		count = len(requested)
	}
	if count > MaxPublicIPAssignments {
		return 0, fmt.Errorf("IP address count cannot exceed %d", MaxPublicIPAssignments)
	}
	return count, nil
}

func EnsureAssignedPublicIPv4s(assignments []config.PublicIPv4Assignment) {
	for _, assignment := range assignments {
		addr := strings.TrimSpace(assignment.Address)
		iface := strings.TrimSpace(assignment.Interface)
		if addr == "" || iface == "" {
			continue
		}
		prefixLen := assignment.PrefixLen
		if prefixLen <= 0 || prefixLen > 32 {
			if info, ok := publicIPv4InfoByAddress(addr); ok && info.PrefixLen > 0 {
				prefixLen = info.PrefixLen
				if iface == "" {
					iface = info.Interface
				}
			}
		}
		if prefixLen <= 0 || prefixLen > 32 {
			prefixLen = 32
		}
		prefixLen = publicIPv4BindingPrefixLen(assignment, prefixLen)
		cidr := fmt.Sprintf("%s/%d", addr, prefixLen)
		if publicIPv4AddressBound(addr, iface) {
			continue
		}
		if output, err := exec.Command("ip", "-4", "addr", "add", cidr, "dev", iface, "label", iface+":clicd").CombinedOutput(); err != nil {
			fmt.Printf("Warning: failed to add assigned public IPv4 %s to %s: %v, output: %s\n", cidr, iface, err, string(output))
		}
	}
}

func publicIPv4BindingPrefixLen(assignment config.PublicIPv4Assignment, prefixLen int) int {
	if prefixLen <= 0 || prefixLen > 32 {
		return 32
	}
	addr, addrErr := netip.ParseAddr(strings.TrimSpace(assignment.Address))
	gateway, gatewayErr := netip.ParseAddr(strings.TrimSpace(assignment.Gateway))
	if addrErr != nil || gatewayErr != nil || !addr.Is4() || !gateway.Is4() {
		return prefixLen
	}
	prefix, err := netip.ParsePrefix(addr.String() + "/" + strconv.Itoa(prefixLen))
	if err != nil {
		return prefixLen
	}
	if !prefix.Masked().Contains(gateway) {
		return 32
	}
	return prefixLen
}

func publicIPv4AddressBound(address string, iface string) bool {
	out, err := exec.Command("ip", "-4", "-o", "addr", "show", "dev", iface).Output()
	if err != nil {
		return false
	}
	for _, line := range strings.Split(string(out), "\n") {
		fields := strings.Fields(line)
		if len(fields) < 4 || fields[2] != "inet" {
			continue
		}
		prefix, err := netip.ParsePrefix(fields[3])
		if err == nil && prefix.Addr().String() == address {
			return true
		}
	}
	return false
}

func EnsureAllAssignedPublicIPv4s() {
	for i := range config.AppConfig.Containers {
		EnsureAssignedPublicIPv4s(config.AppConfig.Containers[i].PublicIPv4s)
	}
}

func publicIPv4InfoByAddress(address string) (PublicIPInfo, bool) {
	for _, info := range DetectPublicIPv4Candidates() {
		if info.Address == address {
			return info, true
		}
	}
	return PublicIPInfo{}, false
}

func detectPublicIPv6Prefixes(defaultRoutes []routeInfo) []IPv6PrefixInfo {
	out, err := exec.Command("ip", "-6", "-o", "addr", "show", "scope", "global").Output()
	if err != nil {
		return nil
	}
	defaultIfaces := map[string]bool{}
	gateways := map[string]string{}
	for _, route := range defaultRoutes {
		defaultIfaces[route.Interface] = true
		if route.Gateway != "" && gateways[route.Interface] == "" {
			gateways[route.Interface] = route.Gateway
		}
	}
	type candidate struct {
		info  IPv6PrefixInfo
		score int
	}
	var candidates []candidate
	seen := map[string]bool{}
	for _, line := range strings.Split(string(out), "\n") {
		fields := strings.Fields(line)
		if len(fields) < 4 || fields[2] != "inet6" {
			continue
		}
		iface := normalizeIface(fields[1])
		if isContainerLikeInterface(iface) {
			continue
		}
		prefix, err := netip.ParsePrefix(fields[3])
		if err != nil || !prefix.Addr().Is6() {
			continue
		}
		addr := prefix.Addr()
		if !isPublicIPv6(addr) {
			continue
		}
		// Require at least 8 host bits. /128 is a single address, not a usable segment.
		if prefix.Bits() > 120 {
			continue
		}
		masked := prefix.Masked()
		key := iface + "|" + masked.String()
		if seen[key] {
			continue
		}
		seen[key] = true
		score := publicInterfaceScore(iface, defaultIfaces)
		if masked.Bits() <= 64 {
			score += 20
		}
		info := IPv6PrefixInfo{
			Interface: iface,
			Address:   addr.String(),
			Prefix:    masked.String(),
			PrefixLen: masked.Bits(),
			Gateway:   gateways[iface],
			IsTunnel:  isTunnelLikeInterface(iface),
			Source:    "local",
		}
		candidates = append(candidates, candidate{info: info, score: score})
	}
	sort.SliceStable(candidates, func(i, j int) bool {
		return candidates[i].score > candidates[j].score
	})
	result := make([]IPv6PrefixInfo, 0, len(candidates))
	for _, c := range candidates {
		result = append(result, c.info)
	}
	return result
}

type routeInfo struct {
	Interface string
	Gateway   string
	Metric    int
}

func detectIPv4DefaultRoutes() []routeInfo {
	out, err := exec.Command("ip", "-4", "route", "show", "default").Output()
	if err != nil {
		return nil
	}
	return parseDefaultRoutes(string(out))
}

func ipv4GatewayForInterface(routes []routeInfo, iface string) string {
	for _, route := range routes {
		if route.Interface == iface && route.Gateway != "" {
			return route.Gateway
		}
	}
	return ""
}

func detectIPv6DefaultRoutes() []routeInfo {
	out, err := exec.Command("ip", "-6", "route", "show", "default").Output()
	if err != nil {
		return nil
	}
	return parseDefaultRoutes(string(out))
}

func parseDefaultRoutes(output string) []routeInfo {
	var routes []routeInfo
	for _, line := range strings.Split(output, "\n") {
		fields := strings.Fields(line)
		if len(fields) == 0 || fields[0] != "default" {
			continue
		}
		route := routeInfo{Metric: 1024}
		for i := 1; i < len(fields)-1; i++ {
			switch fields[i] {
			case "dev":
				route.Interface = normalizeIface(fields[i+1])
			case "via":
				route.Gateway = fields[i+1]
			case "metric":
				metric, err := strconv.Atoi(fields[i+1])
				if err == nil {
					route.Metric = metric
				}
			}
		}
		if route.Interface != "" {
			routes = append(routes, route)
		}
	}
	sort.SliceStable(routes, func(i, j int) bool {
		return routes[i].Metric < routes[j].Metric
	})
	return routes
}

func ipv6ConnectivityOK() bool {
	targets := [][]string{
		{"ping", "-6", "-c", "1", "-W", "2", "2606:4700:4700::1111"},
		{"ping", "-6", "-c", "1", "-W", "2", "2001:4860:4860::8888"},
		{"ping6", "-c", "1", "-W", "2", "2606:4700:4700::1111"},
	}
	for _, args := range targets {
		if exec.Command(args[0], args[1:]...).Run() == nil {
			return true
		}
	}
	return false
}

func isContainerLikeInterface(iface string) bool {
	prefixes := []string{
		"lo", "lxc", "docker", "br-", "veth", "virbr", "cni", "flannel", "cali",
		"kube", "dummy", "ifb", "zt", "zerotier",
	}
	for _, prefix := range prefixes {
		if iface == prefix || strings.HasPrefix(iface, prefix) {
			return true
		}
	}
	return false
}

func normalizeIface(iface string) string {
	iface = strings.TrimSuffix(iface, ":")
	if at := strings.Index(iface, "@"); at >= 0 {
		iface = iface[:at]
	}
	return iface
}

func publicInterfaceScore(iface string, defaultIfaces map[string]bool) int {
	score := 0
	if defaultIfaces[iface] {
		score += 100
	}
	if isTunnelLikeInterface(iface) {
		score -= 120
	} else {
		score += 80
	}
	if isLikelyPhysicalInterface(iface) {
		score += 40
	}
	if operState(iface) == "up" {
		score += 10
	}
	return score
}

func isLikelyPhysicalInterface(iface string) bool {
	prefixes := []string{"eth", "ens", "eno", "enp", "em", "bond", "team"}
	for _, prefix := range prefixes {
		if strings.HasPrefix(iface, prefix) {
			return true
		}
	}
	return false
}

func isTunnelLikeInterface(iface string) bool {
	lower := strings.ToLower(iface)
	prefixes := []string{
		"wg", "wgcf", "warp", "cloudflare", "tun", "tap", "tailscale", "ts",
		"vpn", "ppp", "ipsec", "gre", "gretap", "sit", "he-", "nebula", "zt",
	}
	for _, prefix := range prefixes {
		if lower == prefix || strings.HasPrefix(lower, prefix) {
			return true
		}
	}
	return strings.Contains(lower, "warp") || strings.Contains(lower, "cloudflare")
}

func operState(iface string) string {
	path, err := safeSysClassNetFile(iface, "operstate")
	if err != nil {
		return ""
	}
	data, err := os.ReadFile(path)
	if err != nil {
		return ""
	}
	return strings.TrimSpace(string(data))
}

func safeSysClassNetFile(iface string, filename string) (string, error) {
	iface = strings.TrimSpace(iface)
	filename = strings.TrimSpace(filename)
	if iface == "" || filename == "" || len(iface) > 64 || len(filename) > 64 {
		return "", fmt.Errorf("invalid sysfs network path")
	}
	if iface == "." || iface == ".." || filename == "." || filename == ".." {
		return "", fmt.Errorf("invalid sysfs network path")
	}
	if strings.ContainsAny(iface, "/\\\x00") || strings.ContainsAny(filename, "/\\\x00") {
		return "", fmt.Errorf("invalid sysfs network path")
	}
	base := filepath.Clean("/sys/class/net")
	path := filepath.Clean(filepath.Join(base, iface, filename))
	if !strings.HasPrefix(path, base+string(os.PathSeparator)) {
		return "", fmt.Errorf("invalid sysfs network path")
	}
	return path, nil
}

func isPublicIPv4(addr netip.Addr) bool {
	if !addr.IsGlobalUnicast() || addr.IsPrivate() || addr.IsLoopback() || addr.IsLinkLocalUnicast() {
		return false
	}
	raw := addr.As4()
	if raw[0] == 100 && raw[1] >= 64 && raw[1] <= 127 {
		return false
	}
	if raw[0] == 192 && raw[1] == 0 && raw[2] == 0 {
		return false
	}
	if raw[0] == 192 && raw[1] == 0 && raw[2] == 2 {
		return false
	}
	if raw[0] == 198 && (raw[1] == 18 || raw[1] == 19 || raw[1] == 51 && raw[2] == 100) {
		return false
	}
	if raw[0] == 203 && raw[1] == 0 && raw[2] == 113 {
		return false
	}
	return true
}

func ipv4ToUint32(addr netip.Addr) uint32 {
	raw := addr.As4()
	return binary.BigEndian.Uint32(raw[:])
}

func uint32ToIPv4(value uint32) netip.Addr {
	var raw [4]byte
	binary.BigEndian.PutUint32(raw[:], value)
	return netip.AddrFrom4(raw)
}

func compareIPv4Strings(a, b string) int {
	addrA, errA := netip.ParseAddr(a)
	addrB, errB := netip.ParseAddr(b)
	if errA == nil && errB == nil && addrA.Is4() && addrB.Is4() {
		rawA := addrA.As4()
		rawB := addrB.As4()
		for i := range rawA {
			if rawA[i] < rawB[i] {
				return -1
			}
			if rawA[i] > rawB[i] {
				return 1
			}
		}
		return 0
	}
	return strings.Compare(a, b)
}

func isPublicIPv6(addr netip.Addr) bool {
	if !addr.IsGlobalUnicast() || addr.IsPrivate() || addr.IsLoopback() || addr.IsLinkLocalUnicast() {
		return false
	}
	return !strings.HasPrefix(addr.String(), "2001:db8:")
}

func (m *Manager) allocateIPv6ForContainer(id int) (string, int, string, error) {
	assignments, err := m.allocateIPv6AssignmentsForContainer(id, nil, 1, true)
	if err != nil {
		return "", 0, "", err
	}
	if len(assignments) == 0 {
		return "", 0, "", fmt.Errorf("no free IPv6 address")
	}
	return assignments[0].Address, assignments[0].PrefixLen, assignments[0].Interface, nil
}

func (m *Manager) allocateIPv6AssignmentsForContainer(id int, requested []string, count int, auto bool) ([]config.IPv6Assignment, error) {
	var err error
	count, err = NormalizePublicIPAllocationCount(requested, count)
	if err != nil {
		return nil, err
	}
	status := m.DetectIPv6Status()
	if !status.Available {
		return nil, fmt.Errorf("public IPv6 allocation is unavailable: %s", status.Reason)
	}
	prefixes := make([]struct {
		info   IPv6PrefixInfo
		prefix netip.Prefix
	}, 0, len(status.Prefixes))
	for _, prefixInfo := range status.Prefixes {
		prefix, err := netip.ParsePrefix(prefixInfo.Prefix)
		if err != nil {
			continue
		}
		prefixes = append(prefixes, struct {
			info   IPv6PrefixInfo
			prefix netip.Prefix
		}{info: prefixInfo, prefix: prefix})
	}
	if len(prefixes) == 0 {
		return nil, fmt.Errorf("public IPv6 allocation is unavailable: no valid IPv6 prefix")
	}

	used := map[string]bool{}
	hostAddrs := map[string]bool{}
	for _, p := range status.Prefixes {
		hostAddrs[p.Address] = true
	}
	for _, c := range config.AppConfig.Containers {
		if c.ID == id {
			continue
		}
		if c.IPv6 != "" {
			used[c.IPv6] = true
		}
		for _, ip := range c.IPv6Addresses {
			if ip.Address != "" {
				used[ip.Address] = true
			}
		}
	}

	result := []config.IPv6Assignment{}
	selected := map[string]bool{}
	for _, raw := range requested {
		raw = strings.TrimSpace(raw)
		if raw == "" || selected[raw] {
			continue
		}
		addr, err := netip.ParseAddr(raw)
		if err != nil || !addr.Is6() {
			return nil, fmt.Errorf("requested IPv6 %s is not valid", raw)
		}
		matchIndex := -1
		for i, item := range prefixes {
			if item.prefix.Contains(addr) {
				matchIndex = i
				break
			}
		}
		if matchIndex < 0 {
			return nil, fmt.Errorf("requested IPv6 %s is not in the configured IPv6 prefixes", raw)
		}
		if hostAddrs[raw] {
			return nil, fmt.Errorf("requested IPv6 %s is used by host", raw)
		}
		if used[raw] {
			return nil, fmt.Errorf("requested IPv6 %s is already assigned", raw)
		}
		selected[raw] = true
		used[raw] = true
		result = append(result, config.IPv6Assignment{Address: raw, PrefixLen: prefixes[matchIndex].prefix.Bits(), Interface: prefixes[matchIndex].info.Interface})
	}

	if len(result) >= count || !auto {
		return result, nil
	}

	for _, item := range prefixes {
		for offset := uint64(0x1000 + id); offset < 0x100000; offset++ {
			addr, err := ipv6Add(item.prefix.Masked().Addr(), offset)
			if err != nil || !item.prefix.Contains(addr) {
				break
			}
			candidate := addr.String()
			if !used[candidate] && !hostAddrs[candidate] {
				used[candidate] = true
				result = append(result, config.IPv6Assignment{Address: candidate, PrefixLen: item.prefix.Bits(), Interface: item.info.Interface})
				if len(result) >= count {
					return result, nil
				}
			}
		}
	}
	return nil, fmt.Errorf("no free IPv6 address in configured prefixes")
}

func ipv6Add(base netip.Addr, offset uint64) (netip.Addr, error) {
	raw := base.As16()
	value := big.NewInt(0).SetBytes(raw[:])
	add := make([]byte, 8)
	binary.BigEndian.PutUint64(add, offset)
	value.Add(value, big.NewInt(0).SetBytes(add))
	bytes := value.Bytes()
	if len(bytes) > 16 {
		return netip.Addr{}, fmt.Errorf("IPv6 address overflow")
	}
	padded := make([]byte, 16)
	copy(padded[16-len(bytes):], bytes)
	var out [16]byte
	copy(out[:], padded)
	return netip.AddrFrom16(out), nil
}

func ipv6AssignmentAddresses(assignments []config.IPv6Assignment) []string {
	values := make([]string, 0, len(assignments))
	for _, item := range assignments {
		if strings.TrimSpace(item.Address) != "" {
			values = append(values, strings.TrimSpace(item.Address))
		}
	}
	return values
}

func normalizeIPv6List(values []string) []string {
	seen := map[string]bool{}
	result := make([]string, 0, len(values))
	for _, value := range values {
		value = strings.TrimSpace(value)
		if value == "" || seen[value] {
			continue
		}
		seen[value] = true
		result = append(result, value)
	}
	return result
}

func shellQuotedIPv6List(values []string) string {
	values = normalizeIPv6List(values)
	if len(values) == 0 {
		return "''"
	}
	quoted := make([]string, 0, len(values))
	for _, value := range values {
		quoted = append(quoted, shellQuote(value))
	}
	return strings.Join(quoted, " ")
}

func (m *Manager) AssignIPv6(id int) (*config.Container, error) {
	c := config.FindContainer(id)
	if c == nil {
		return nil, fmt.Errorf("container not found: %d", id)
	}
	if c.IPv6 == "" {
		assignments, err := m.allocateIPv6AssignmentsForContainer(id, nil, 1, true)
		if err != nil {
			return nil, err
		}
		c.IPv6Addresses = append(c.IPv6Addresses, assignments...)
		c.NormalizeNetworkAssignments()
		config.SaveConfig()
	}
	if err := m.applyIPv6Config(c.LxcName(), c.IPv6AddressStrings()...); err != nil {
		return nil, err
	}
	rootfsPath := filepath.Join(m.LxcPath, c.LxcName(), "rootfs")
	if _, err := os.Stat(rootfsPath); err == nil {
		if err := installContainerIPv6Init(rootfsPath, c.IPv6AddressStrings()...); err != nil {
			fmt.Printf("Warning: failed to install IPv6 init in %s: %v\n", c.LxcName(), err)
		}
	}
	if err := m.ApplyIPv6(id); err != nil {
		return nil, err
	}
	return c, nil
}

func (m *Manager) applyIPv6Config(lxcName string, ipv6s ...string) error {
	configFile := filepath.Join(m.LxcPath, lxcName, "config")
	data, err := os.ReadFile(configFile)
	if err != nil {
		return fmt.Errorf("failed to read container config: %v", err)
	}
	lines := strings.Split(string(data), "\n")
	next := make([]string, 0, len(lines)+4)
	for _, line := range lines {
		trimmed := strings.TrimSpace(line)
		if strings.Contains(trimmed, "# clicd managed: public IPv6") ||
			strings.HasPrefix(trimmed, "lxc.net.0.ipv6.address") ||
			strings.HasPrefix(trimmed, "lxc.net.0.ipv6.gateway") {
			continue
		}
		next = append(next, line)
	}
	ipv6s = normalizeIPv6List(ipv6s)
	if len(ipv6s) > 0 {
		next = append(next, "", "# clicd managed: public IPv6 routed /128")
		for _, ipv6 := range ipv6s {
			next = append(next, fmt.Sprintf("lxc.net.0.ipv6.address = %s/128", ipv6))
		}
		next = append(next, "lxc.net.0.ipv6.gateway = auto")
	}
	return os.WriteFile(configFile, []byte(strings.Join(next, "\n")), 0644)
}

func (m *Manager) ApplyIPv6(id int) error {
	c := config.FindContainer(id)
	if c == nil {
		return fmt.Errorf("container not found: %d", id)
	}
	if c.IPv6 == "" && len(c.IPv6Addresses) == 0 {
		return nil
	}
	c.NormalizeNetworkAssignments()
	if c.IPv6Interface == "" {
		status := m.DetectIPv6Status()
		if len(status.Prefixes) == 0 {
			return fmt.Errorf("failed to detect IPv6 uplink for %s", c.IPv6)
		}
		c.IPv6Interface = status.Prefixes[0].Interface
		c.IPv6PrefixLen = status.Prefixes[0].PrefixLen
		for i := range c.IPv6Addresses {
			if c.IPv6Addresses[i].Interface == "" {
				c.IPv6Addresses[i].Interface = c.IPv6Interface
			}
			if c.IPv6Addresses[i].PrefixLen == 0 {
				c.IPv6Addresses[i].PrefixLen = c.IPv6PrefixLen
			}
		}
		config.SaveConfig()
	}

	rootfsPath := filepath.Join(m.LxcPath, c.LxcName(), "rootfs")
	if _, err := os.Stat(rootfsPath); err == nil {
		if err := installContainerIPv6Init(rootfsPath, c.IPv6AddressStrings()...); err != nil {
			fmt.Printf("Warning: failed to install IPv6 init in %s: %v\n", c.LxcName(), err)
		}
	}
	for _, assignment := range c.IPv6Addresses {
		uplink := assignment.Interface
		if uplink == "" {
			uplink = c.IPv6Interface
		}
		if err := ensureHostIPv6Routing(assignment.Address, uplink); err != nil {
			return err
		}
	}
	status, _ := m.GetContainerStatus(c.LxcName())
	if status != "running" {
		return nil
	}
	addrs := shellQuotedIPv6List(c.IPv6AddressStrings())
	cmd := exec.Command("lxc-attach", "-n", c.LxcName(), "--", "sh", "-c",
		fmt.Sprintf("for ip in %s; do ip -6 addr replace \"$ip/128\" dev eth0; done && ip -6 route replace default via %s dev eth0 metric 100",
			addrs, shellQuote(ipv6GatewayLinkLocal)))
	output, err := cmd.CombinedOutput()
	if err != nil {
		return fmt.Errorf("failed to apply IPv6 inside container: %v, output: %s", err, string(output))
	}
	for _, assignment := range c.IPv6Addresses {
		uplink := assignment.Interface
		if uplink == "" {
			uplink = c.IPv6Interface
		}
		removeIPv6NAT66(assignment.Address, uplink)
	}
	if !containerIPv6ConnectivityOK(c.LxcName()) {
		for _, assignment := range c.IPv6Addresses {
			uplink := assignment.Interface
			if uplink == "" {
				uplink = c.IPv6Interface
			}
			ensureIPv6NAT66(assignment.Address, uplink)
		}
	}
	if err := ApplyFirewallRules(c.ID); err != nil {
		fmt.Printf("Warning: failed to re-apply firewall rules after IPv6 setup for %s: %v\n", c.Name, err)
	}
	return nil
}

func ensureHostIPv6Routing(ipv6, uplink string) error {
	if uplink == "" {
		return fmt.Errorf("missing IPv6 uplink interface")
	}
	runQuiet("sysctl", "-w", "net.ipv6.conf.all.forwarding=1")
	runQuiet("sysctl", "-w", "net.ipv6.conf."+uplink+".accept_ra=2")
	runQuiet("sysctl", "-w", "net.ipv6.conf."+uplink+".proxy_ndp=1")
	runQuiet("ip", "link", "set", "lxcbr0", "up")
	runQuiet("ip", "-6", "addr", "add", ipv6GatewayLinkLocal+"/64", "dev", "lxcbr0")
	if out, err := exec.Command("ip", "-6", "route", "replace", ipv6+"/128", "dev", "lxcbr0").CombinedOutput(); err != nil {
		return fmt.Errorf("failed to add IPv6 host route: %v, output: %s", err, string(out))
	}
	if out, err := exec.Command("ip", "-6", "neigh", "replace", "proxy", ipv6, "dev", uplink).CombinedOutput(); err != nil {
		return fmt.Errorf("failed to add IPv6 proxy NDP: %v, output: %s", err, string(out))
	}
	ensureIPv6ForwardRules(ipv6)
	return nil
}

func installContainerIPv6Init(rootfsPath string, ipv6s ...string) error {
	ipv6s = normalizeIPv6List(ipv6s)
	if len(ipv6s) == 0 {
		return nil
	}
	for _, ipv6 := range ipv6s {
		if _, err := netip.ParseAddr(ipv6); err != nil {
			return fmt.Errorf("invalid IPv6 address %q: %w", ipv6, err)
		}
	}

	scriptPath := filepath.Join(rootfsPath, "usr", "local", "sbin", "clicd-ipv6-init")
	if err := os.MkdirAll(filepath.Dir(scriptPath), 0755); err != nil {
		return err
	}
	script := `#!/bin/sh
IPV6_ADDRS="` + strings.Join(ipv6s, " ") + `"
IPV6_GW=` + shellQuote(ipv6GatewayLinkLocal) + `
IFACE="${CLICD_IPV6_IFACE:-eth0}"

command -v ip >/dev/null 2>&1 || exit 0

i=0
while [ "$i" -lt 30 ]; do
	if ip link show dev "$IFACE" >/dev/null 2>&1; then
		break
	fi
	i=$((i + 1))
	sleep 1
done

ip link set dev "$IFACE" up >/dev/null 2>&1 || true
for IPV6_ADDR in $IPV6_ADDRS; do
	ip -6 addr replace "$IPV6_ADDR/128" dev "$IFACE" >/dev/null 2>&1 || true
done
ip -6 route replace default via "$IPV6_GW" dev "$IFACE" metric 100 >/dev/null 2>&1 || true
exit 0
`
	if err := os.WriteFile(scriptPath, []byte(script), 0755); err != nil {
		return err
	}

	osRelease := ""
	if data, err := os.ReadFile(filepath.Join(rootfsPath, "etc", "os-release")); err == nil {
		osRelease = strings.ToLower(string(data))
	}
	hasSystemd := dirExists(filepath.Join(rootfsPath, "etc", "systemd", "system"))
	hasOpenRC := fileExists(filepath.Join(rootfsPath, "sbin", "openrc-run")) || strings.Contains(osRelease, "alpine")

	if hasSystemd {
		if err := installContainerIPv6Systemd(rootfsPath); err != nil {
			return err
		}
	}
	if hasOpenRC {
		if err := installContainerIPv6OpenRC(rootfsPath); err != nil {
			return err
		}
	}
	if !hasSystemd && !hasOpenRC {
		if err := installContainerIPv6SysV(rootfsPath); err != nil {
			return err
		}
	}
	return nil
}

func installContainerIPv6Systemd(rootfsPath string) error {
	servicePath := filepath.Join(rootfsPath, "etc", "systemd", "system", "clicd-ipv6.service")
	if err := os.MkdirAll(filepath.Dir(servicePath), 0755); err != nil {
		return err
	}
	service := `[Unit]
Description=CLICD IPv6 setup
After=network-online.target network.target
Wants=network-online.target

[Service]
Type=oneshot
ExecStart=/usr/local/sbin/clicd-ipv6-init
RemainAfterExit=yes

[Install]
WantedBy=multi-user.target
`
	if err := os.WriteFile(servicePath, []byte(service), 0644); err != nil {
		return err
	}
	wantsDir := filepath.Join(rootfsPath, "etc", "systemd", "system", "multi-user.target.wants")
	if err := os.MkdirAll(wantsDir, 0755); err != nil {
		return err
	}
	return replaceSymlink("../clicd-ipv6.service", filepath.Join(wantsDir, "clicd-ipv6.service"))
}

func installContainerIPv6OpenRC(rootfsPath string) error {
	initPath := filepath.Join(rootfsPath, "etc", "init.d", "clicd-ipv6")
	if err := os.MkdirAll(filepath.Dir(initPath), 0755); err != nil {
		return err
	}
	initScript := `#!/sbin/openrc-run
name="CLICD IPv6 setup"
description="Apply CLICD IPv6 settings"

depend() {
	after net networking
	need net
}

start() {
	ebegin "Applying CLICD IPv6"
	/usr/local/sbin/clicd-ipv6-init
	eend $?
}
`
	if err := os.WriteFile(initPath, []byte(initScript), 0755); err != nil {
		return err
	}
	runlevelDir := filepath.Join(rootfsPath, "etc", "runlevels", "default")
	if err := os.MkdirAll(runlevelDir, 0755); err != nil {
		return err
	}
	return replaceSymlink(filepath.Join("..", "..", "init.d", "clicd-ipv6"), filepath.Join(runlevelDir, "clicd-ipv6"))
}

func installContainerIPv6SysV(rootfsPath string) error {
	initDir := filepath.Join(rootfsPath, "etc", "init.d")
	if err := os.MkdirAll(initDir, 0755); err != nil {
		return err
	}
	initPath := filepath.Join(initDir, "clicd-ipv6")
	initScript := `#!/bin/sh
### BEGIN INIT INFO
# Provides:          clicd-ipv6
# Required-Start:    $network
# Required-Stop:
# Default-Start:     2 3 4 5
# Default-Stop:
# Short-Description: CLICD IPv6 setup
### END INIT INFO

case "$1" in
	start|restart|force-reload)
		/usr/local/sbin/clicd-ipv6-init
		;;
	stop|status)
		exit 0
		;;
	*)
		echo "Usage: $0 {start|stop|restart|force-reload|status}"
		exit 1
		;;
esac
exit 0
`
	if err := os.WriteFile(initPath, []byte(initScript), 0755); err != nil {
		return err
	}
	for _, level := range []string{"2", "3", "4", "5"} {
		rcDir := filepath.Join(rootfsPath, "etc", "rc"+level+".d")
		if !dirExists(rcDir) {
			continue
		}
		if err := replaceSymlink(filepath.Join("..", "init.d", "clicd-ipv6"), filepath.Join(rcDir, "S99clicd-ipv6")); err != nil {
			return err
		}
	}
	return nil
}

func replaceSymlink(target, linkPath string) error {
	if current, err := os.Readlink(linkPath); err == nil && current == target {
		return nil
	}
	if err := os.Remove(linkPath); err != nil && !os.IsNotExist(err) {
		return err
	}
	return os.Symlink(target, linkPath)
}

func fileExists(path string) bool {
	info, err := os.Stat(path)
	return err == nil && !info.IsDir()
}

func dirExists(path string) bool {
	info, err := os.Stat(path)
	return err == nil && info.IsDir()
}

func ensureIPv6ForwardRules(ipv6 string) {
	rules := [][]string{
		{"FORWARD", "-i", "lxcbr0", "-s", ipv6 + "/128", "-j", "ACCEPT"},
		{"FORWARD", "-o", "lxcbr0", "-d", ipv6 + "/128", "-j", "ACCEPT"},
	}
	for _, rule := range rules {
		check := append([]string{"-C"}, rule...)
		add := append([]string{"-A"}, rule...)
		if exec.Command("ip6tables", check...).Run() != nil {
			exec.Command("ip6tables", add...).Run()
		}
	}
}

func removeHostIPv6Routing(ipv6, uplink string) {
	removeIPv6NAT66(ipv6, uplink)
	removeIPv6ForwardRules(ipv6)
	runQuiet("ip", "-6", "route", "del", ipv6+"/128", "dev", "lxcbr0")
	if uplink != "" {
		runQuiet("ip", "-6", "neigh", "del", "proxy", ipv6, "dev", uplink)
	}
}

func removeIPv6ForwardRules(ipv6 string) {
	rules := [][]string{
		{"FORWARD", "-i", "lxcbr0", "-s", ipv6 + "/128", "-j", "ACCEPT"},
		{"FORWARD", "-o", "lxcbr0", "-d", ipv6 + "/128", "-j", "ACCEPT"},
	}
	for _, rule := range rules {
		del := append([]string{"-D"}, rule...)
		for exec.Command("ip6tables", del...).Run() == nil {
		}
	}
}

func containerIPv6ConnectivityOK(lxcName string) bool {
	targets := []string{"2606:4700:4700::1111", "2001:4860:4860::8888"}
	for _, target := range targets {
		if exec.Command("lxc-attach", "-n", lxcName, "--", "ping", "-6", "-c", "1", "-W", "2", target).Run() == nil {
			return true
		}
	}
	return false
}

func ensureIPv6NAT66(ipv6, uplink string) {
	if ipv6 == "" || uplink == "" {
		return
	}
	rule := []string{"POSTROUTING", "-s", ipv6 + "/128", "-o", uplink, "-j", "MASQUERADE"}
	check := append([]string{"-t", "nat", "-C"}, rule...)
	add := append([]string{"-t", "nat", "-A"}, rule...)
	if exec.Command("ip6tables", check...).Run() != nil {
		exec.Command("ip6tables", add...).Run()
	}
}

func removeIPv6NAT66(ipv6, uplink string) {
	if ipv6 == "" || uplink == "" {
		return
	}
	rule := []string{"POSTROUTING", "-s", ipv6 + "/128", "-o", uplink, "-j", "MASQUERADE"}
	del := append([]string{"-t", "nat", "-D"}, rule...)
	for exec.Command("ip6tables", del...).Run() == nil {
	}
}

func runQuiet(name string, args ...string) {
	_ = exec.Command(name, args...).Run()
}

func (m *Manager) AssignedIPv6Count() int {
	count := 0
	for _, c := range config.AppConfig.Containers {
		if strings.TrimSpace(c.IPv6) != "" {
			count++
		}
	}
	return count
}

func IPv6PrefixCapacity(prefixLen int) string {
	if prefixLen <= 0 || prefixLen > 128 {
		return "0"
	}
	hostBits := 128 - prefixLen
	if hostBits > 32 {
		return "large"
	}
	return strconv.FormatUint(uint64(1)<<uint(hostBits), 10)
}
