package lxc

import (
	"bufio"
	"context"
	"crypto/rand"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
	"reflect"
	"regexp"
	"strconv"
	"strings"
	"sync"
	"time"

	"clicd/internal/config"
)

// Manager handles LXC container operations
type Manager struct {
	LxcPath string
}

type containerUsageSample struct {
	CPUUsec    uint64
	RXBytes    uint64
	TXBytes    uint64
	ReadBytes  uint64
	WriteBytes uint64
	At         time.Time
}

// containerRateSnapshot stores computed rates, updated by a single background goroutine
type containerRateSnapshot struct {
	CPUPct    float64
	RXBps     float64
	TXBps     float64
	ReadBps   float64
	WriteBps  float64
	UpdatedAt time.Time
}

var (
	usageMu   sync.RWMutex
	lastUsage = map[string]containerUsageSample{}
	rateCache = map[string]containerRateSnapshot{}
)

var (
	sshEnsureLocks     sync.Map
	sshWarmupScheduled sync.Map
	sshWarmupSem       = make(chan struct{}, 4)
)

// StartUsageMonitor starts a background goroutine that computes rates every 5 seconds
func (m *Manager) StartUsageMonitor() {
	go func() {
		for {
			time.Sleep(5 * time.Second)
			m.updateAllRates()
		}
	}()
}

// WarmRunningContainersSSH prepares sshd for containers that were already running
// when clicd started, such as after host boot or service restart.
func (m *Manager) WarmRunningContainersSSH() {
	containers := append([]config.Container(nil), config.AppConfig.Containers...)
	for _, container := range containers {
		c := container
		if c.IsKVM() {
			continue
		}
		status, err := m.GetContainerStatus(c.LxcName())
		if err != nil || status != "running" {
			continue
		}
		config.UpdateContainerStatus(c.ID, "running")
		if c.IP != "" && m.containerPortListening(c.LxcName(), 22) {
			continue
		}
		m.WarmSSHAsync(c.ID, "running container scan")
	}
}

// StartSSHWarmupScanner repeatedly scans during service startup so containers
// that autostart slightly after clicd still get prepared before WebSSH opens.
func (m *Manager) StartSSHWarmupScanner() {
	go func() {
		for i := 0; i < 12; i++ {
			m.WarmRunningContainersSSH()
			time.Sleep(10 * time.Second)
		}
	}()
}

func (m *Manager) updateAllRates() {
	usageMu.Lock()
	defer usageMu.Unlock()

	for i := range config.AppConfig.Containers {
		c := &config.AppConfig.Containers[i]
		if c.IsKVM() {
			delete(lastUsage, c.VirshName())
			delete(rateCache, c.VirshName())
			continue
		}
		if c.Status != "running" {
			delete(lastUsage, c.LxcName())
			delete(rateCache, c.LxcName())
			continue
		}
		lxcName := c.LxcName()

		// Cache init PID once per scan so getContainerNetworkBytes / getContainerDiskIOBytes
		// don't each fork lxc-info separately.
		initPID := m.getContainerInitPID(lxcName)

		// Read memory from cgroup directly (no shell fork)
		memUsage := readCgroupFile(lxcName,
			"/sys/fs/cgroup/lxc/%s/memory.current",
			"/sys/fs/cgroup/lxc.payload.%s/memory.current",
			"/sys/fs/cgroup/memory/lxc/%s/memory.usage_in_bytes")

		// Read cpu usage from cgroup directly (no shell | awk fork)
		cpuUsec := readCgroupCPUUsec(lxcName,
			"/sys/fs/cgroup/lxc/%s/cpu.stat",
			"/sys/fs/cgroup/lxc.payload.%s/cpu.stat")

		rxBytes, txBytes := getNetworkBytesForPID(initPID)
		readBytes, writeBytes := getDiskIOBytesForPID(initPID)

		now := time.Now()
		sample := containerUsageSample{
			CPUUsec:    cpuUsec,
			RXBytes:    rxBytes,
			TXBytes:    txBytes,
			ReadBytes:  readBytes,
			WriteBytes: writeBytes,
			At:         now,
		}

		prev, exists := lastUsage[lxcName]
		lastUsage[lxcName] = sample

		rate := containerRateSnapshot{UpdatedAt: now}
		if exists {
			elapsed := sample.At.Sub(prev.At).Seconds()
			if elapsed > 0 && sample.CPUUsec >= prev.CPUUsec {
				rate.CPUPct = float64(sample.CPUUsec-prev.CPUUsec) / (elapsed * 1e6) * 100
			}
			if elapsed > 0 {
				if sample.RXBytes >= prev.RXBytes {
					rate.RXBps = float64(sample.RXBytes-prev.RXBytes) / elapsed
				}
				if sample.TXBytes >= prev.TXBytes {
					rate.TXBps = float64(sample.TXBytes-prev.TXBytes) / elapsed
				}
				if sample.ReadBytes >= prev.ReadBytes {
					rate.ReadBps = float64(sample.ReadBytes-prev.ReadBytes) / elapsed
				}
				if sample.WriteBytes >= prev.WriteBytes {
					rate.WriteBps = float64(sample.WriteBytes-prev.WriteBytes) / elapsed
				}
			}
		} else {
			// First sample: estimate from container uptime
			uptimeSec := m.getContainerUptimeSeconds(lxcName)
			if uptimeSec > 0 {
				rate.CPUPct = (float64(cpuUsec) / 1e6) / uptimeSec * 100
				rate.RXBps = float64(rxBytes) / uptimeSec
				rate.TXBps = float64(txBytes) / uptimeSec
				rate.ReadBps = float64(readBytes) / uptimeSec
				rate.WriteBps = float64(writeBytes) / uptimeSec
			}
		}

		// Fallback: if delta rate is 0 but cumulative has data, use cumulative average
		if rate.RXBps == 0 && rateCache[lxcName].RXBps > 0 {
			rate.RXBps = rateCache[lxcName].RXBps
		}
		if rate.TXBps == 0 && rateCache[lxcName].TXBps > 0 {
			rate.TXBps = rateCache[lxcName].TXBps
		}

		// Store memory as a rate field for convenience
		_ = memUsage
		rateCache[lxcName] = rate
	}

	// Clean up stale entries
	for name := range rateCache {
		found := false
		for i := range config.AppConfig.Containers {
			if config.AppConfig.Containers[i].LxcName() == name && config.AppConfig.Containers[i].Status == "running" {
				found = true
				break
			}
		}
		if !found {
			delete(rateCache, name)
			delete(lastUsage, name)
		}
	}
}

// NewManager creates a new LXC manager
func NewManager() *Manager {
	return &Manager{
		LxcPath: "/var/lib/lxc",
	}
}

// ContainerConfig defines container creation parameters
type ContainerConfig struct {
	Name             string   `json:"name"`
	Virtualization   string   `json:"virtualization,omitempty"`
	TemplateID       string   `json:"template_id"`
	VCPU             float64  `json:"vcpu"`
	CPUPercent       int      `json:"cpu_percent"`
	RAMMB            int      `json:"ram_mb"`
	DiskGB           int      `json:"disk_gb"`
	NetworkBWMbps    int      `json:"network_bw_mbps"`
	NetworkDownMbps  int      `json:"network_down_mbps"`
	NetworkUpMbps    int      `json:"network_up_mbps"`
	MonthlyTrafficGB int      `json:"monthly_traffic_gb"`
	TrafficMode      string   `json:"traffic_mode"`   // "total" or "in_out"
	TrafficInGB      int      `json:"traffic_in_gb"`  // 0=unlimited
	TrafficOutGB     int      `json:"traffic_out_gb"` // 0=unlimited
	IOSpeedMBps      int      `json:"io_speed_mbps"`
	IOReadMBps       int      `json:"io_read_mbps"`
	IOWriteMBps      int      `json:"io_write_mbps"`
	ExtraPorts       []int    `json:"extra_ports"`
	PortMappingCount int      `json:"port_mapping_count"`
	AssignNAT        *bool    `json:"assign_nat,omitempty"`
	SnapshotLimit    int      `json:"snapshot_limit"`
	AssignIPv4       bool     `json:"assign_ipv4"`
	IPv4Count        int      `json:"ipv4_count,omitempty"`
	PublicIPv4s      []string `json:"public_ipv4s,omitempty"`
	AssignIPv6       bool     `json:"assign_ipv6"`
	IPv6Count        int      `json:"ipv6_count,omitempty"`
	IPv6Addresses    []string `json:"ipv6_addresses,omitempty"`
	SSHAuthMode      string   `json:"ssh_auth_mode,omitempty"`
	SSHPassword      string   `json:"ssh_password,omitempty"`
	SSHPublicKey     string   `json:"ssh_public_key,omitempty"`
	ExpiresAt        string   `json:"expires_at"`
}

func (cfg *ContainerConfig) NormalizeResourceAliases() {
	if cfg == nil {
		return
	}
	if cfg.NetworkBWMbps < 0 {
		cfg.NetworkBWMbps = 0
	}
	if cfg.NetworkDownMbps < 0 {
		cfg.NetworkDownMbps = 0
	}
	if cfg.NetworkUpMbps < 0 {
		cfg.NetworkUpMbps = 0
	}
	if cfg.NetworkDownMbps == 0 && cfg.NetworkUpMbps == 0 && cfg.NetworkBWMbps > 0 {
		cfg.NetworkDownMbps = cfg.NetworkBWMbps
		cfg.NetworkUpMbps = cfg.NetworkBWMbps
	}
	cfg.NetworkBWMbps = config.LegacySymmetricLimit(cfg.NetworkDownMbps, cfg.NetworkUpMbps)

	if cfg.IOSpeedMBps < 0 {
		cfg.IOSpeedMBps = 0
	}
	if cfg.IOReadMBps < 0 {
		cfg.IOReadMBps = 0
	}
	if cfg.IOWriteMBps < 0 {
		cfg.IOWriteMBps = 0
	}
	if cfg.IOReadMBps == 0 && cfg.IOWriteMBps == 0 && cfg.IOSpeedMBps > 0 {
		cfg.IOReadMBps = cfg.IOSpeedMBps
		cfg.IOWriteMBps = cfg.IOSpeedMBps
	}
	cfg.IOSpeedMBps = config.LegacySymmetricLimit(cfg.IOReadMBps, cfg.IOWriteMBps)
}

func (cfg ContainerConfig) WantsNAT() bool {
	return cfg.AssignNAT == nil || *cfg.AssignNAT
}

// CreateContainer creates a new LXC container. Uses ct-{id} as LXC name internally.
func (m *Manager) CreateContainer(cfg ContainerConfig) error {
	cfg.NormalizeResourceAliases()
	tmpl := FindTemplate(cfg.TemplateID)
	if tmpl == nil {
		return fmt.Errorf("template not found: %s", cfg.TemplateID)
	}
	if cfg.WantsNAT() && cfg.PortMappingCount < 2 {
		cfg.PortMappingCount = 2
	} else if !cfg.WantsNAT() {
		cfg.PortMappingCount = 0
		cfg.ExtraPorts = nil
	}
	if cfg.SnapshotLimit <= 0 {
		cfg.SnapshotLimit = config.DefaultSnapshotLimit
	}

	if !config.IsValidContainerName(cfg.Name) {
		return fmt.Errorf("invalid container name: %s", cfg.Name)
	}
	if config.FindContainerByName(cfg.Name) != nil {
		return fmt.Errorf("container name already exists: %s", cfg.Name)
	}
	sshAccess, err := ResolveCreateSSHAccess(cfg)
	if err != nil {
		return err
	}

	// Allocate ID and build LXC name
	id := config.AllocateContainerID()
	lxcName := fmt.Sprintf("ct-%d", id)

	containerDir := filepath.Join(m.LxcPath, lxcName)
	if _, err := os.Stat(containerDir); err == nil {
		if err := m.cleanupContainerStorage(lxcName); err != nil {
			return fmt.Errorf("failed to clean stale container directory %s: %v", lxcName, err)
		}
	} else if !os.IsNotExist(err) {
		return fmt.Errorf("failed to check container directory %s: %v", lxcName, err)
	}

	fmt.Printf("Creating LXC container: %s (ID=%d, template: %s/%s/%s)\n",
		lxcName, id, tmpl.Distro, tmpl.Release, tmpl.Arch)

	args := []string{"-n", lxcName, "-t", "download", "--",
		"-d", tmpl.Distro, "-r", tmpl.Release, "-a", tmpl.Arch}
	if tmpl.Variant != "" {
		args = append(args, "--variant", tmpl.Variant)
	}
	cmd := exec.Command("lxc-create", args...)
	output, err := cmd.CombinedOutput()
	if err != nil {
		return fmt.Errorf("lxc-create failed: %v, output: %s", err, string(output))
	}

	if err := m.applyDiskLimit(lxcName, cfg.DiskGB); err != nil {
		_ = m.cleanupContainerStorage(lxcName)
		return err
	}

	// Apply resource limits and mandatory security hardening.
	if err := m.applyResourceLimits(lxcName, cfg); err != nil {
		_ = m.cleanupContainerStorage(lxcName)
		return err
	}

	publicIPv4s, err := AllocatePublicIPv4Assignments(id, cfg.PublicIPv4s, cfg.IPv4Count, cfg.AssignIPv4)
	if err != nil {
		_ = m.cleanupContainerStorage(lxcName)
		return err
	}

	ipv6Assignments := []config.IPv6Assignment{}
	if cfg.AssignIPv6 || len(cfg.IPv6Addresses) > 0 {
		assigned, err := m.allocateIPv6AssignmentsForContainer(id, cfg.IPv6Addresses, cfg.IPv6Count, true)
		if err != nil {
			_ = m.cleanupContainerStorage(lxcName)
			return err
		}
		ipv6Assignments = assigned
		if err := m.applyIPv6Config(lxcName, ipv6AssignmentAddresses(ipv6Assignments)...); err != nil {
			_ = m.cleanupContainerStorage(lxcName)
			return err
		}
	}

	sshPassword := sshAccess.Password

	sshPort := 0
	portMappings := []config.PortMapping{}
	if cfg.WantsNAT() {
		sshPort = config.AllocateSSHPort()

		// Setup default port mappings (SSH only)
		portMappings = SetupDefaultPortMappings(sshPort)
		// NAT4 port mappings should bind to the host IP, not the container's independent public IPv4.
		tempC := &config.Container{ID: id, PublicIPv4s: publicIPv4s, PortMappings: portMappings}

		extraPorts := cfg.ExtraPorts
		if len(extraPorts) == 0 && cfg.PortMappingCount > 1 {
			extraPorts = allocateDefaultEqualPorts(tempC, cfg.PortMappingCount-1)
		}
		for _, containerPort := range extraPorts {
			if containerPort <= 0 {
				continue
			}
			pm, err := normalizePortMapping(tempC, -1, config.PortMapping{
				ContainerPort: containerPort,
				HostPort:      containerPort,
				HostIP:        "",
				Protocol:      "tcp",
				Description:   fmt.Sprintf("Port-%d", containerPort),
			})
			if err != nil {
				continue
			}
			tempC.PortMappings = append(tempC.PortMappings, pm)
			portMappings = tempC.PortMappings
		}
	}

	now := time.Now().Format("2006-01-02 15:04:05")
	// Determine traffic mode
	trafficMode := cfg.TrafficMode
	if trafficMode == "" {
		trafficMode = "total"
	}
	trafficResetDate := now[:7] // YYYY-MM for monthly tracking

	container := config.Container{
		ID:               id,
		UUID:             config.NewContainerUUID(),
		Name:             cfg.Name,
		Virtualization:   config.VirtualizationLXC,
		Template:         cfg.TemplateID,
		VCPU:             cfg.VCPU,
		RAMMB:            cfg.RAMMB,
		DiskGB:           cfg.DiskGB,
		NetworkBWMbps:    cfg.NetworkBWMbps,
		NetworkDownMbps:  cfg.NetworkDownMbps,
		NetworkUpMbps:    cfg.NetworkUpMbps,
		MonthlyTrafficGB: cfg.MonthlyTrafficGB,
		TrafficMode:      trafficMode,
		TrafficInGB:      cfg.TrafficInGB,
		TrafficOutGB:     cfg.TrafficOutGB,
		TrafficResetDate: trafficResetDate,
		IOSpeedMBps:      cfg.IOSpeedMBps,
		IOReadMBps:       cfg.IOReadMBps,
		IOWriteMBps:      cfg.IOWriteMBps,
		Status:           "stopped",
		IP:               "",
		PublicIPv4s:      publicIPv4s,
		IPv6Addresses:    ipv6Assignments,
		VNCPort:          0,
		SSHPort:          sshPort,
		SSHPassword:      sshPassword,
		PortMappings:     portMappings,
		PortMappingLimit: cfg.PortMappingCount,
		SnapshotLimit:    config.NormalizeSnapshotLimit(cfg.SnapshotLimit),
		CreatedAt:        now,
		ExpiresAt:        cfg.ExpiresAt,
	}
	container.NormalizeNetworkAssignments()
	config.AddContainer(container)

	// Pre-configure network and SSH in the rootfs before first boot.
	rootfsPath := filepath.Join(m.LxcPath, lxcName, "rootfs")
	m.preconfigureNetwork(rootfsPath, cfg.TemplateID)
	if len(ipv6Assignments) > 0 {
		if err := installContainerIPv6Init(rootfsPath, ipv6AssignmentAddresses(ipv6Assignments)...); err != nil {
			fmt.Printf("Warning: failed to install IPv6 init in %s: %v\n", lxcName, err)
		}
	}
	if err := m.preconfigureSSH(rootfsPath, cfg.TemplateID, sshAccess.Mode); err != nil {
		fmt.Printf("Warning: failed to pre-configure SSH in %s: %v\n", lxcName, err)
	}
	if sshAccess.PublicKey != "" {
		if err := m.installRootAuthorizedKey(rootfsPath, sshAccess.PublicKey); err != nil {
			_ = m.cleanupContainerStorage(lxcName)
			config.RemoveContainer(id)
			return fmt.Errorf("failed to install SSH public key: %v", err)
		}
	}

	if err := m.shiftRootfsForUnprivileged(lxcName); err != nil {
		_ = m.cleanupContainerStorage(lxcName)
		config.RemoveContainer(id)
		return err
	}

	// Set root password AFTER shiftRootfsForUnprivileged,
	// otherwise /etc/shadow ownership breaks and SSHD cannot authenticate.
	if err := m.setRootfsPassword(rootfsPath, sshPassword); err != nil {
		fmt.Printf("Warning: failed to set root password in %s: %v\n", lxcName, err)
	}

	fmt.Printf("Container %d (%s) created successfully\n", id, cfg.Name)
	return nil
}

func (m *Manager) preconfigureNetwork(rootfsPath, templateID string) {
	osRelease := ""
	if data, err := os.ReadFile(filepath.Join(rootfsPath, "etc", "os-release")); err == nil {
		osRelease = strings.ToLower(string(data))
	}
	isAlpine := strings.Contains(osRelease, "alpine")
	isRHELFamily := strings.Contains(osRelease, "centos") ||
		strings.Contains(osRelease, "rhel") ||
		strings.Contains(osRelease, "rocky") ||
		strings.Contains(osRelease, "alma") ||
		strings.Contains(osRelease, "fedora") ||
		strings.Contains(templateID, "fedora") ||
		strings.Contains(templateID, "rockylinux")

	if isAlpine {
		interfaces := filepath.Join(rootfsPath, "etc", "network", "interfaces")
		content := "auto lo\niface lo inet loopback\n\nauto eth0\niface eth0 inet dhcp\n"
		_ = os.MkdirAll(filepath.Dir(interfaces), 0755)
		_ = os.WriteFile(interfaces, []byte(content), 0644)
		_ = m.runRootfsCommand(rootfsPath, "rc-update", "add", "networking", "boot")
		return
	}

	if isRHELFamily || strings.Contains(templateID, "centos") {
		nmDir := filepath.Join(rootfsPath, "etc", "NetworkManager", "system-connections")
		if err := os.MkdirAll(nmDir, 0700); err == nil {
			keyfile := `[connection]
id=eth0
type=ethernet
interface-name=eth0
autoconnect=true

[ipv4]
method=auto

[ipv6]
method=ignore
`
			path := filepath.Join(nmDir, "eth0.nmconnection")
			_ = os.WriteFile(path, []byte(keyfile), 0600)
		}
		_ = m.runRootfsCommand(rootfsPath, "systemctl", "enable", "NetworkManager")
	}

	networkdDir := filepath.Join(rootfsPath, "etc", "systemd", "network")
	if err := os.MkdirAll(networkdDir, 0755); err == nil {
		network := `[Match]
Name=eth0

[Network]
DHCP=ipv4
IPv6AcceptRA=no
`
		_ = os.WriteFile(filepath.Join(networkdDir, "10-eth0.network"), []byte(network), 0644)
	}
	if !isRHELFamily {
		_ = m.runRootfsCommand(rootfsPath, "systemctl", "enable", "systemd-networkd")
	}
}

// preconfigureSSH installs and configures SSH directly in the rootfs before first boot.
func (m *Manager) preconfigureSSH(rootfsPath, templateID string, sshAuthMode string) error {
	_ = templateID
	// Disable pubkey auth when user chose password-only mode (password or auto_password)
	disablePubkey := sshAuthMode == SSHAuthPassword || sshAuthMode == SSHAuthAutoPassword
	ctx, cancel := context.WithTimeout(context.Background(), 180*time.Second)
	defer cancel()
	cmd, err := m.rootfsCommand(rootfsPath, "sh", "-c", sshSetupScript(false, disablePubkey))
	if err != nil {
		return err
	}
	cmd = exec.CommandContext(ctx, cmd.Path, cmd.Args[1:]...)
	output, err := cmd.CombinedOutput()
	if ctx.Err() == context.DeadlineExceeded {
		return fmt.Errorf("timed out after 180s, output: %s", string(output))
	}
	if err != nil {
		return fmt.Errorf("%v, output: %s", err, string(output))
	}
	fmt.Printf("SSH pre-configured in rootfs\n")
	return nil
}

// applyResourceLimits applies cgroup v2 limits and mandatory security hardening to container config.
func (m *Manager) applyResourceLimits(lxcName string, cfg ContainerConfig) error {
	cfg.NormalizeResourceAliases()
	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)
	}
	content := string(data)

	lines := strings.Split(content, "\n")
	var newLines []string
	for _, line := range lines {
		trimmed := strings.TrimSpace(line)
		if !strings.Contains(trimmed, "# clicd managed") &&
			!strings.HasPrefix(trimmed, "lxc.cgroup2.memory.max") &&
			!strings.HasPrefix(trimmed, "lxc.cgroup2.cpuset.cpus") &&
			!strings.HasPrefix(trimmed, "lxc.cgroup2.cpu.max") &&
			!strings.HasPrefix(trimmed, "lxc.cgroup2.io.max") &&
			!strings.HasPrefix(trimmed, "lxc.mount.auto") &&
			!strings.HasPrefix(trimmed, "lxc.prlimit") &&
			!strings.HasPrefix(trimmed, "lxc.idmap") &&
			!strings.HasPrefix(trimmed, "lxc.apparmor.profile") &&
			!strings.HasPrefix(trimmed, "lxc.seccomp.profile") &&
			!strings.HasPrefix(trimmed, "lxc.no_new_privs") &&
			!strings.HasPrefix(trimmed, "lxc.cap.drop") {
			newLines = append(newLines, line)
		}
	}

	seccompProfile, err := findSeccompProfile()
	if err != nil {
		return err
	}
	apparmorProfile, err := appArmorProfileForTemplate(cfg.TemplateID)
	if err != nil {
		return err
	}
	uidBase, gidBase, err := unprivilegedIDMap()
	if err != nil {
		return err
	}

	newLines = append(newLines, "", "# clicd managed: lxcfs virtualized /proc")
	newLines = append(newLines, "lxc.mount.auto = proc:mixed sys:mixed cgroup:mixed")
	newLines = append(newLines, "", "# clicd managed: mandatory unprivileged container hardening")
	newLines = append(newLines, fmt.Sprintf("lxc.idmap = u 0 %d 65536", uidBase))
	newLines = append(newLines, fmt.Sprintf("lxc.idmap = g 0 %d 65536", gidBase))
	newLines = append(newLines, fmt.Sprintf("lxc.apparmor.profile = %s", apparmorProfile))
	newLines = append(newLines, fmt.Sprintf("lxc.seccomp.profile = %s", seccompProfile))
	newLines = append(newLines, "lxc.no_new_privs = 1")
	// Keep sys_admin: unprivileged containers need it to mount tmpfs (/dev/shm, /run, etc.)
	// All capabilities are already confined to the container's user namespace.
	newLines = append(newLines, "lxc.cap.drop = mac_admin mac_override sys_module sys_rawio sys_time sys_boot sys_nice sys_resource sys_ptrace sys_pacct mknod audit_control audit_read")
	newLines = append(newLines, "lxc.prlimit.nofile = 1024:4096")
	newLines = append(newLines, "lxc.prlimit.nproc = 128:256")
	newLines = append(newLines, "", "# clicd managed resource limits (cgroup v2)")

	if cfg.VCPU > 0 {
		cpuPct := cfg.CPUPercent
		if cpuPct <= 0 || cpuPct > 100 {
			cpuPct = 100
		}
		cpuQuota := int(cfg.VCPU * float64(cpuPct) / 100.0 * 100000)
		newLines = append(newLines, fmt.Sprintf("lxc.cgroup2.cpu.max = %d 100000", cpuQuota))
	}
	if cfg.RAMMB > 0 {
		ramBytes := int64(cfg.RAMMB) * 1024 * 1024
		newLines = append(newLines, fmt.Sprintf("lxc.cgroup2.memory.max = %d", ramBytes))
	}
	if cfg.IOReadMBps > 0 || cfg.IOWriteMBps > 0 {
		// Note: lxc.cgroup2.io.max is skipped for unprivileged containers because
		// LXC's cgfsng_setup_limits cannot resolve host device numbers (e.g. 8:1)
		// in the unprivileged namespace context.
		// IO limits are instead applied post-start via direct cgroup2 writes.
		fmt.Printf("Info: IO limit (read=%d MB/s write=%d MB/s) for %s will be applied post-start via cgroup2\n", cfg.IOReadMBps, cfg.IOWriteMBps, lxcName)
	}

	newContent := strings.Join(newLines, "\n")
	if err := os.WriteFile(configFile, []byte(newContent), 0644); err != nil {
		return fmt.Errorf("failed to write container config: %v", err)
	}
	return nil
}

func (m *Manager) ioLimitLines(lxcName string, readMBps int, writeMBps int) ([]string, error) {
	if readMBps < 0 {
		readMBps = 0
	}
	if writeMBps < 0 {
		writeMBps = 0
	}
	devices, err := m.rootfsBlockDevices(lxcName)
	if err != nil {
		return nil, err
	}
	readValue := "max"
	if readMBps > 0 {
		readValue = strconv.Itoa(readMBps * 1024 * 1024)
	}
	writeValue := "max"
	if writeMBps > 0 {
		writeValue = strconv.Itoa(writeMBps * 1024 * 1024)
	}
	lines := make([]string, 0, len(devices))
	for _, device := range devices {
		lines = append(lines, fmt.Sprintf("%s rbps=%s wbps=%s", device, readValue, writeValue))
	}
	return lines, nil
}

func (m *Manager) rootfsBlockDevices(lxcName string) ([]string, error) {
	rootfsPath := filepath.Join(m.LxcPath, lxcName, "rootfs")
	out, err := exec.Command("findmnt", "-T", rootfsPath, "-no", "MAJ:MIN").Output()
	if err != nil {
		return nil, fmt.Errorf("failed to detect rootfs block device for IO limit: %v", err)
	}
	re := regexp.MustCompile(`\b\d+:\d+\b`)
	matches := re.FindAllString(string(out), -1)
	seen := map[string]bool{}
	devices := make([]string, 0, len(matches))
	for _, match := range matches {
		if match == "0:0" || seen[match] {
			continue
		}
		seen[match] = true
		devices = append(devices, match)
	}
	if len(devices) == 0 {
		return nil, fmt.Errorf("failed to detect rootfs block device for IO limit; refusing to use hardcoded 8:0")
	}
	return devices, nil
}

func (m *Manager) applyDiskLimit(lxcName string, diskGB int) error {
	if diskGB <= 0 {
		return nil
	}
	return m.applyLoopbackDiskLimit(lxcName, diskGB)
}

func (m *Manager) applyProjectDiskLimit(lxcName string, diskGB int) error {
	if diskGB <= 0 {
		return nil
	}
	rootfsPath := filepath.Join(m.LxcPath, lxcName, "rootfs")
	if diskImageMounted(lxcName, rootfsPath) {
		return nil
	}
	fsType, err := findmntValue(rootfsPath, "FSTYPE")
	if err != nil {
		return fmt.Errorf("failed to detect rootfs filesystem for disk quota: %v", err)
	}
	fsType = strings.TrimSpace(fsType)

	switch fsType {
	case "btrfs":
		if err := exec.Command("btrfs", "quota", "enable", rootfsPath).Run(); err != nil {
			// btrfs returns an error when quota is already enabled on some versions.
			fmt.Printf("Warning: btrfs quota enable returned: %v\n", err)
		}
		output, err := exec.Command("btrfs", "qgroup", "limit", fmt.Sprintf("%dG", diskGB), rootfsPath).CombinedOutput()
		if err != nil {
			fmt.Printf("Warning: failed to apply btrfs disk quota, falling back to loopback rootfs: %v, output: %s\n", err, string(output))
			return m.applyLoopbackDiskLimit(lxcName, diskGB)
		}
		return nil
	case "xfs":
		if err := applyXFSProjectQuota(rootfsPath, lxcName, diskGB); err != nil {
			fmt.Printf("Warning: xfs project quota unavailable, falling back to loopback rootfs: %v\n", err)
			return m.applyLoopbackDiskLimit(lxcName, diskGB)
		}
		return nil
	case "ext4":
		if err := applyExt4ProjectQuota(rootfsPath, lxcName, diskGB); err != nil {
			fmt.Printf("Warning: ext4 project quota unavailable, falling back to loopback rootfs: %v\n", err)
			return m.applyLoopbackDiskLimit(lxcName, diskGB)
		}
		return nil
	default:
		fmt.Printf("Warning: unsupported rootfs filesystem %q for project quota, falling back to loopback rootfs\n", fsType)
		return m.applyLoopbackDiskLimit(lxcName, diskGB)
	}
}

func (m *Manager) applyLoopbackDiskLimit(lxcName string, diskGB int) error {
	containerDir := filepath.Join(m.LxcPath, lxcName)
	rootfsPath := filepath.Join(containerDir, "rootfs")
	imagePath := filepath.Join(containerDir, "rootfs.img")
	if diskImageMounted(lxcName, rootfsPath) {
		return nil
	}
	if _, err := os.Stat(imagePath); err == nil {
		return m.ensureDiskImageMounted(lxcName)
	}

	tmpMount := filepath.Join(containerDir, ".rootfs-image")
	backupRootfs := filepath.Join(containerDir, "rootfs.dir")
	if err := os.MkdirAll(tmpMount, 0755); err != nil {
		return err
	}
	defer os.RemoveAll(tmpMount)

	output, err := exec.Command("truncate", "-s", fmt.Sprintf("%dG", diskGB), imagePath).CombinedOutput()
	if err != nil {
		return fmt.Errorf("failed to create rootfs disk image: %v, output: %s", err, string(output))
	}
	output, err = exec.Command("mkfs.ext4", "-F", imagePath).CombinedOutput()
	if err != nil {
		return fmt.Errorf("failed to format rootfs disk image: %v, output: %s", err, string(output))
	}
	output, err = exec.Command("mount", "-o", "loop", imagePath, tmpMount).CombinedOutput()
	if err != nil {
		return fmt.Errorf("failed to mount rootfs disk image: %v, output: %s", err, string(output))
	}
	mountedTmp := true
	defer func() {
		if mountedTmp {
			exec.Command("umount", "-l", tmpMount).Run()
		}
	}()

	output, err = exec.Command("cp", "-a", rootfsPath+string(os.PathSeparator)+".", tmpMount+string(os.PathSeparator)).CombinedOutput()
	if err != nil {
		return fmt.Errorf("failed to copy rootfs into disk image: %v, output: %s", err, string(output))
	}
	if !rootfsHasInit(tmpMount) {
		return fmt.Errorf("failed to copy rootfs into disk image: init not found in prepared rootfs")
	}
	if output, err = exec.Command("umount", tmpMount).CombinedOutput(); err != nil {
		return fmt.Errorf("failed to unmount prepared rootfs disk image: %v, output: %s", err, string(output))
	}
	mountedTmp = false

	if err := os.Rename(rootfsPath, backupRootfs); err != nil {
		return fmt.Errorf("failed to move original rootfs aside: %v", err)
	}
	if err := os.MkdirAll(rootfsPath, 0755); err != nil {
		os.Rename(backupRootfs, rootfsPath)
		return err
	}
	if err := m.ensureDiskImageMounted(lxcName); err != nil {
		os.RemoveAll(rootfsPath)
		os.Rename(backupRootfs, rootfsPath)
		return err
	}
	if err := os.RemoveAll(backupRootfs); err != nil {
		fmt.Printf("Warning: failed to remove old rootfs backup %s: %v\n", backupRootfs, err)
	}
	return nil
}

func (m *Manager) ensureDiskImageMounted(lxcName string) error {
	containerDir := filepath.Join(m.LxcPath, lxcName)
	rootfsPath := filepath.Join(containerDir, "rootfs")
	imagePath := filepath.Join(containerDir, "rootfs.img")
	if _, err := os.Stat(imagePath); os.IsNotExist(err) {
		return nil
	}
	if diskImageMounted(lxcName, rootfsPath) {
		return nil
	}
	if err := os.MkdirAll(rootfsPath, 0755); err != nil {
		return err
	}
	output, err := exec.Command("mount", "-o", "loop", imagePath, rootfsPath).CombinedOutput()
	if err != nil {
		return fmt.Errorf("failed to mount rootfs disk image: %v, output: %s", err, string(output))
	}
	return nil
}

func rootfsHasInit(rootfsPath string) bool {
	for _, rel := range []string{
		"sbin/init",
		"usr/lib/systemd/systemd",
		"lib/systemd/systemd",
		"bin/busybox",
		"bin/sh",
	} {
		if _, err := os.Stat(filepath.Join(rootfsPath, rel)); err == nil {
			return true
		}
	}
	// NixOS has init under a hash-named nix store path; check nix/store for any init
	nixStore := filepath.Join(rootfsPath, "nix", "store")
	if entries, err := os.ReadDir(nixStore); err == nil {
		for _, entry := range entries {
			if entry.IsDir() && strings.Contains(entry.Name(), "nixos-system-") {
				if _, err := os.Stat(filepath.Join(nixStore, entry.Name(), "init")); err == nil {
					return true
				}
				if _, err := os.Stat(filepath.Join(nixStore, entry.Name(), "systemd")); err == nil {
					return true
				}
			}
		}
	}
	return false
}

func diskImageMounted(lxcName, rootfsPath string) bool {
	_ = lxcName
	target, err := findmntValue(rootfsPath, "TARGET")
	if err != nil {
		return false
	}
	targetAbs, err := filepath.Abs(strings.TrimSpace(target))
	if err != nil {
		return false
	}
	rootfsAbs, err := filepath.Abs(rootfsPath)
	if err != nil {
		return false
	}
	return targetAbs == rootfsAbs
}

func applyXFSProjectQuota(rootfsPath, lxcName string, diskGB int) error {
	options, err := findmntValue(rootfsPath, "OPTIONS")
	if err != nil {
		return err
	}
	if !hasProjectQuotaOption(options) {
		return errors.New("xfs project quota is not enabled; remount with prjquota before creating containers")
	}
	mountPoint, err := findmntValue(rootfsPath, "TARGET")
	if err != nil {
		return err
	}
	projectID := projectQuotaID(lxcName)
	projectName := "clicd-" + lxcName
	if err := ensureProjectQuotaFiles(projectID, projectName, rootfsPath); err != nil {
		return err
	}
	output, err := exec.Command("xfs_quota", "-x",
		"-c", "project -s "+projectName,
		"-c", fmt.Sprintf("limit -p bhard=%dg %s", diskGB, projectName),
		mountPoint,
	).CombinedOutput()
	if err != nil {
		return fmt.Errorf("failed to apply xfs project quota: %v, output: %s", err, string(output))
	}
	return nil
}

func applyExt4ProjectQuota(rootfsPath, lxcName string, diskGB int) error {
	options, err := findmntValue(rootfsPath, "OPTIONS")
	if err != nil {
		return err
	}
	if !hasProjectQuotaOption(options) {
		return errors.New("ext4 project quota is not enabled; remount with prjquota before creating containers")
	}
	mountPoint, err := findmntValue(rootfsPath, "TARGET")
	if err != nil {
		return err
	}
	projectID := projectQuotaID(lxcName)
	output, err := exec.Command("chattr", "-p", strconv.Itoa(projectID), rootfsPath).CombinedOutput()
	if err != nil {
		return fmt.Errorf("failed to assign ext4 project id: %v, output: %s", err, string(output))
	}
	hardKB := diskGB * 1024 * 1024
	output, err = exec.Command("setquota", "-P", strconv.Itoa(projectID), "0", strconv.Itoa(hardKB), "0", "0", mountPoint).CombinedOutput()
	if err != nil {
		return fmt.Errorf("failed to apply ext4 project quota: %v, output: %s", err, string(output))
	}
	return nil
}

func findmntValue(path, field string) (string, error) {
	out, err := exec.Command("findmnt", "-T", path, "-no", field).Output()
	if err != nil {
		return "", err
	}
	value := strings.TrimSpace(string(out))
	if value == "" {
		return "", fmt.Errorf("findmnt returned empty %s for %s", field, path)
	}
	return value, nil
}

func hasProjectQuotaOption(options string) bool {
	for _, option := range strings.Split(options, ",") {
		option = strings.TrimSpace(option)
		if option == "prjquota" || option == "pquota" || option == "project" {
			return true
		}
	}
	return false
}

func projectQuotaID(lxcName string) int {
	idPart := strings.TrimPrefix(lxcName, "ct-")
	id, err := strconv.Atoi(idPart)
	if err != nil {
		id = 1
	}
	return 200000 + id
}

func ensureProjectQuotaFiles(projectID int, projectName, rootfsPath string) error {
	projectsLine := fmt.Sprintf("%d:%s", projectID, rootfsPath)
	if err := appendUniqueLine("/etc/projects", projectsLine); err != nil {
		return err
	}
	projidLine := fmt.Sprintf("%s:%d", projectName, projectID)
	return appendUniqueLine("/etc/projid", projidLine)
}

func appendUniqueLine(path, line string) error {
	data, _ := os.ReadFile(path)
	for _, existing := range strings.Split(string(data), "\n") {
		if strings.TrimSpace(existing) == line {
			return nil
		}
	}
	f, err := os.OpenFile(path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
	if err != nil {
		return err
	}
	defer f.Close()
	if len(data) > 0 && !strings.HasSuffix(string(data), "\n") {
		if _, err := f.WriteString("\n"); err != nil {
			return err
		}
	}
	_, err = f.WriteString(line + "\n")
	return err
}

func findSeccompProfile() (string, error) {
	for _, path := range []string{
		"/usr/share/lxc/config/common.seccomp",
		"/usr/share/lxc/config/common.seccomp.policy",
		"/etc/lxc/common.seccomp",
	} {
		if _, err := os.Stat(path); err == nil {
			return path, nil
		}
	}
	return "", errors.New("required LXC seccomp profile not found")
}

func findAppArmorProfile() (string, error) {
	data, err := os.ReadFile("/sys/kernel/security/apparmor/profiles")
	if err != nil {
		return "", fmt.Errorf("apparmor is required but not available: %v", err)
	}
	profiles := string(data)
	for _, profile := range []string{"lxc-container-default-cgns", "lxc-container-default"} {
		if strings.Contains(profiles, profile+" ") || strings.Contains(profiles, profile+" (") {
			return profile, nil
		}
	}
	return "", errors.New("required LXC AppArmor profile not loaded")
}

func appArmorProfileForTemplate(templateID string) (string, error) {
	if systemdTemplateNeedsUnconfinedAppArmor(templateID) {
		return "unconfined", nil
	}
	return findAppArmorProfile()
}

func systemdTemplateNeedsUnconfinedAppArmor(templateID string) bool {
	id := strings.ToLower(strings.TrimSpace(templateID))
	if id == "" || strings.Contains(id, "alpine") {
		return false
	}
	for _, token := range []string{"ubuntu", "debian", "centos", "fedora", "rocky", "rockylinux", "archlinux"} {
		if strings.Contains(id, token) {
			return true
		}
	}
	return false
}

func unprivilegedIDMap() (int, int, error) {
	if err := ensureSubIDRange("/etc/subuid", "root", 100000, 65536); err != nil {
		return 0, 0, err
	}
	if err := ensureSubIDRange("/etc/subgid", "root", 100000, 65536); err != nil {
		return 0, 0, err
	}
	uidBase, err := parseSubIDRange("/etc/subuid", "root")
	if err != nil {
		return 0, 0, err
	}
	gidBase, err := parseSubIDRange("/etc/subgid", "root")
	if err != nil {
		return 0, 0, err
	}
	return uidBase, gidBase, nil
}

func ensureSubIDRange(path, user string, start, count int) error {
	if _, err := parseSubIDRange(path, user); err == nil {
		return nil
	}
	f, err := os.OpenFile(path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
	if err != nil {
		return fmt.Errorf("failed to update %s: %v", path, err)
	}
	defer f.Close()
	if _, err := f.WriteString(fmt.Sprintf("%s:%d:%d\n", user, start, count)); err != nil {
		return fmt.Errorf("failed to update %s: %v", path, err)
	}
	return nil
}

func parseSubIDRange(path, user string) (int, error) {
	data, err := os.ReadFile(path)
	if err != nil {
		return 0, fmt.Errorf("failed to read %s: %v", path, err)
	}
	for _, line := range strings.Split(string(data), "\n") {
		fields := strings.Split(strings.TrimSpace(line), ":")
		if len(fields) != 3 || fields[0] != user {
			continue
		}
		start, startErr := strconv.Atoi(fields[1])
		count, countErr := strconv.Atoi(fields[2])
		if startErr == nil && countErr == nil && count >= 65536 {
			return start, nil
		}
	}
	return 0, fmt.Errorf("%s must contain a %s subordinate id range with at least 65536 ids", path, user)
}

func (m *Manager) ensureUnprivilegedLXCPathAccess(lxcName string) error {
	// Unprivileged container root maps to a subordinate host UID, so it needs
	// execute permission on the LXC parent and container directories to reach
	// rootfs. Some distributions create /var/lib/lxc as 750/700, which causes
	// lxc-start to abort with "Could not access /var/lib/lxc".
	for _, path := range []string{m.LxcPath, filepath.Join(m.LxcPath, lxcName)} {
		info, err := os.Stat(path)
		if err != nil {
			return err
		}
		mode := info.Mode().Perm()
		if mode&0001 != 0 {
			continue
		}
		if err := os.Chmod(path, mode|0001); err != nil {
			return fmt.Errorf("failed to fix LXC path permissions for %s: %v", path, err)
		}
	}
	return nil
}

func (m *Manager) shiftRootfsForUnprivileged(lxcName string) error {
	uidBase, gidBase, err := unprivilegedIDMap()
	if err != nil {
		return err
	}
	rootfsPath := filepath.Join(m.LxcPath, lxcName, "rootfs")
	marker := filepath.Join(rootfsPath, ".clicd-unprivileged-shifted")
	if err := m.ensureUnprivilegedLXCPathAccess(lxcName); err != nil {
		return err
	}
	if _, err := os.Stat(marker); err == nil {
		return nil
	}
	m.unmountRootfsChildMounts(rootfsPath)
	rootInfo, err := os.Lstat(rootfsPath)
	if err != nil {
		return err
	}
	rootDev, _, _, ok := fileStatFields(rootInfo)
	if !ok {
		return fmt.Errorf("failed to read rootfs device for %s", rootfsPath)
	}

	if err := filepath.WalkDir(rootfsPath, func(path string, _ os.DirEntry, walkErr error) error {
		if walkErr != nil {
			return walkErr
		}
		info, err := os.Lstat(path)
		if err != nil {
			return err
		}
		dev, uid, gid, ok := fileStatFields(info)
		if !ok {
			return fmt.Errorf("failed to read uid/gid for %s", path)
		}
		if path != rootfsPath && dev != rootDev {
			if info.IsDir() {
				return filepath.SkipDir
			}
			return nil
		}
		if uid >= uidBase && uid < uidBase+65536 && gid >= gidBase && gid < gidBase+65536 {
			return nil
		}
		if uid >= 0 && uid < 65536 {
			uid += uidBase
		}
		if gid >= 0 && gid < 65536 {
			gid += gidBase
		}
		return os.Lchown(path, uid, gid)
	}); err != nil {
		return fmt.Errorf("failed to shift rootfs ownership for unprivileged LXC: %v", err)
	}

	if err := os.WriteFile(marker, []byte("1\n"), 0644); err != nil {
		return err
	}
	if err := os.Lchown(marker, uidBase, gidBase); err != nil {
		return err
	}

	// Fix container directory permissions: unprivileged init runs as ns UID 0
	// (host UID 100000), which is "other" on the host. lxc-create sets the
	// container dir to 770, so we need o+x to let the container process
	// traverse into the directory and access rootfs.
	containerDir := filepath.Join(m.LxcPath, lxcName)
	if err := os.Chmod(containerDir, 0771); err != nil {
		return fmt.Errorf("failed to fix container directory permissions: %v", err)
	}
	return nil
}

func fileStatFields(info os.FileInfo) (dev uint64, uid int, gid int, ok bool) {
	if info == nil || info.Sys() == nil {
		return 0, 0, 0, false
	}
	stat := reflect.ValueOf(info.Sys())
	if stat.Kind() == reflect.Pointer {
		if stat.IsNil() {
			return 0, 0, 0, false
		}
		stat = stat.Elem()
	}
	if stat.Kind() != reflect.Struct {
		return 0, 0, 0, false
	}
	devValue, devOK := numericField(stat, "Dev")
	uidValue, uidOK := numericField(stat, "Uid")
	gidValue, gidOK := numericField(stat, "Gid")
	if !devOK || !uidOK || !gidOK {
		return 0, 0, 0, false
	}
	return devValue, int(uidValue), int(gidValue), true
}

func numericField(v reflect.Value, name string) (uint64, bool) {
	field := v.FieldByName(name)
	if !field.IsValid() {
		return 0, false
	}
	switch field.Kind() {
	case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
		return field.Uint(), true
	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
		value := field.Int()
		if value < 0 {
			return 0, false
		}
		return uint64(value), true
	default:
		return 0, false
	}
}

func (m *Manager) unmountRootfsChildMounts(rootfsPath string) {
	rootAbs, err := filepath.Abs(rootfsPath)
	if err != nil {
		return
	}
	out, err := exec.Command("findmnt", "-R", "-n", "-o", "TARGET", rootfsPath).Output()
	if err != nil {
		return
	}
	targets := strings.Split(strings.TrimSpace(string(out)), "\n")
	for i, j := 0, len(targets)-1; i < j; i, j = i+1, j-1 {
		targets[i], targets[j] = targets[j], targets[i]
	}
	for _, target := range targets {
		target = strings.TrimSpace(target)
		if target == "" {
			continue
		}
		targetAbs, err := filepath.Abs(target)
		if err != nil || targetAbs == rootAbs {
			continue
		}
		if strings.HasPrefix(targetAbs, rootAbs+string(os.PathSeparator)) {
			exec.Command("umount", "-R", "-l", targetAbs).Run()
		}
	}
}

func (m *Manager) rootfsShifted(lxcName string) bool {
	marker := filepath.Join(m.LxcPath, lxcName, "rootfs", ".clicd-unprivileged-shifted")
	_, err := os.Stat(marker)
	return err == nil
}

func (m *Manager) hasUnprivilegedIDMap(lxcName string) bool {
	data, err := os.ReadFile(filepath.Join(m.LxcPath, lxcName, "config"))
	if err != nil {
		return false
	}
	content := string(data)
	return strings.Contains(content, "lxc.idmap = u 0 ") && strings.Contains(content, "lxc.idmap = g 0 ")
}

// StartContainer starts an LXC container by its ID
func (m *Manager) StartContainer(id int) error {
	c := config.FindContainer(id)
	if c == nil {
		return fmt.Errorf("container not found: %d", id)
	}
	lxcName := c.LxcName()

	if err := m.ensureDiskImageMounted(lxcName); err != nil {
		return err
	}
	if m.hasUnprivilegedIDMap(lxcName) && !m.rootfsShifted(lxcName) {
		if err := m.shiftRootfsForUnprivileged(lxcName); err != nil {
			return err
		}
	}
	if m.rootfsShifted(lxcName) {
		if err := m.applyResourceLimits(lxcName, ContainerConfig{
			Name:             c.Name,
			TemplateID:       c.Template,
			VCPU:             c.VCPU,
			RAMMB:            c.RAMMB,
			DiskGB:           c.DiskGB,
			NetworkBWMbps:    c.NetworkBWMbps,
			NetworkDownMbps:  c.NetworkDownMbps,
			NetworkUpMbps:    c.NetworkUpMbps,
			MonthlyTrafficGB: c.MonthlyTrafficGB,
			IOSpeedMBps:      c.IOSpeedMBps,
			IOReadMBps:       c.IOReadMBps,
			IOWriteMBps:      c.IOWriteMBps,
			AssignIPv6:       c.IPv6 != "" || len(c.IPv6Addresses) > 0,
			ExpiresAt:        c.ExpiresAt,
		}); err != nil {
			return err
		}
	}
	if c.IPv6 != "" || len(c.IPv6Addresses) > 0 {
		c.NormalizeNetworkAssignments()
		if err := m.applyIPv6Config(lxcName, c.IPv6AddressStrings()...); err != nil {
			return err
		}
		if err := m.ApplyIPv6(id); err != nil {
			return err
		}
	}
	EnsureAssignedPublicIPv4s(c.PublicIPv4s)

	logFile, consoleLog, output, err := m.startLXCContainerDaemon(lxcName)
	if err != nil {
		config.UpdateContainerStatus(id, "stopped")
		return fmt.Errorf("failed to start container: %v, output: %s, lxc log: %s, console: %s", err, string(output), tailFile(logFile, 80), tailFile(consoleLog, 80))
	}
	if err := m.waitForLXCStartup(lxcName, logFile, consoleLog); err != nil {
		config.UpdateContainerStatus(id, "stopped")
		return err
	}

	config.UpdateContainerStatus(id, "running")

	var ip string
	for retry := 0; retry < 10; retry++ {
		time.Sleep(2 * time.Second)
		ip, err = m.GetContainerIP(lxcName)
		if err == nil && ip != "" {
			break
		}
	}
	if ip != "" {
		c = config.FindContainer(id)
		if c != nil {
			c.IP = ip
			config.SaveConfig()
		}
	}
	if ip == "" {
		if repairedIP, repairErr := m.EnsureContainerIPv4(id); repairErr == nil && repairedIP != "" {
			ip = repairedIP
			c = config.FindContainer(id)
		} else if repairErr != nil {
			fmt.Printf("Warning: failed to prepare IPv4 for %s: %v\n", lxcName, repairErr)
		}
	}

	if ip != "" {
		if err := m.EnsureSSH(id); err != nil {
			return err
		}
	}

	if current := config.FindContainer(id); current != nil {
		c = current
	}
	if err := m.ApplyContainerLimits(c); err != nil {
		fmt.Printf("Warning: failed to apply runtime resource limits for %s: %v\n", lxcName, err)
	}

	if err := m.ApplyPortMappings(id); err != nil {
		fmt.Printf("Warning: failed to apply port mappings: %v\n", err)
	}
	if err := ApplyFirewallRules(id); err != nil {
		fmt.Printf("Warning: failed to apply firewall rules: %v\n", err)
	}
	if c.IPv6 != "" || len(c.IPv6Addresses) > 0 {
		if err := m.ApplyIPv6(id); err != nil {
			fmt.Printf("Warning: failed to apply IPv6 routing for %s: %v\n", lxcName, err)
		}
	}

	fmt.Printf("Container %d (%s) started, IP: %s\n", id, c.Name, ip)
	return nil
}

func (m *Manager) startLXCContainerDaemon(lxcName string) (string, string, []byte, error) {
	logFile := filepath.Join(os.TempDir(), "clicd-"+lxcName+"-start.log")
	consoleLog := filepath.Join(os.TempDir(), "clicd-"+lxcName+"-console.log")
	os.Remove(logFile)
	os.Remove(consoleLog)
	cmd := exec.Command("lxc-start", "-n", lxcName, "-d", "--logfile", logFile, "--logpriority", "DEBUG", "--console-log", consoleLog)
	output, err := cmd.CombinedOutput()
	return logFile, consoleLog, output, err
}

func (m *Manager) waitForLXCStartup(lxcName, logFile, consoleLog string) error {
	runningChecks := 0
	lastStatus := "unknown"
	for retry := 0; retry < 10; retry++ {
		time.Sleep(1 * time.Second)
		status, err := m.GetContainerStatus(lxcName)
		if err != nil {
			lastStatus = "unknown"
			continue
		}
		lastStatus = status
		if status == "running" {
			runningChecks++
			if runningChecks >= 3 {
				return nil
			}
			continue
		}
		if runningChecks > 0 || retry >= 1 {
			break
		}
	}
	return fmt.Errorf("container exited immediately after start (status: %s), lxc log: %s, console: %s", lastStatus, tailFile(logFile, 80), tailFile(consoleLog, 80))
}

// applyBandwidthLimit applies tc-based bandwidth limit on container's veth interface
// ApplyContainerLimits re-applies resource limits (CPU, RAM, IO, BW) to a running container.
func (m *Manager) ApplyContainerLimits(c *config.Container) error {
	if c == nil || c.Status != "running" {
		return nil
	}
	config.NormalizeContainerResourceAliases(c)
	lxcName := c.LxcName()

	// CPU: write cpu.max
	cpuQuota := int(c.VCPU * 100000)
	cpuLine := fmt.Sprintf("%d 100000", cpuQuota)
	for _, path := range []string{
		fmt.Sprintf("/sys/fs/cgroup/lxc/%s/cpu.max", lxcName),
		fmt.Sprintf("/sys/fs/cgroup/lxc.payload.%s/cpu.max", lxcName),
	} {
		os.WriteFile(path, []byte(cpuLine), 0644)
	}

	// Memory: write memory.max
	ramBytes := int64(c.RAMMB) * 1024 * 1024
	memLine := fmt.Sprintf("%d", ramBytes)
	for _, path := range []string{
		fmt.Sprintf("/sys/fs/cgroup/lxc/%s/memory.max", lxcName),
		fmt.Sprintf("/sys/fs/cgroup/lxc.payload.%s/memory.max", lxcName),
	} {
		os.WriteFile(path, []byte(memLine), 0644)
	}

	// IO speed: write io.max, including max values to clear old per-direction limits.
	ioLines, err := m.ioLimitLines(lxcName, c.IOReadMBps, c.IOWriteMBps)
	if err != nil {
		return err
	}
	ioLine := strings.Join(ioLines, "\n")
	for _, path := range []string{
		fmt.Sprintf("/sys/fs/cgroup/lxc/%s/io.max", lxcName),
		fmt.Sprintf("/sys/fs/cgroup/lxc.payload.%s/io.max", lxcName),
	} {
		os.WriteFile(path, []byte(ioLine), 0644)
	}

	// Network bandwidth
	m.applyBandwidthLimit(lxcName, c.NetworkDownMbps, c.NetworkUpMbps)
	return nil
}

func (m *Manager) applyBandwidthLimit(lxcName string, downMbps int, upMbps int) {
	veth := m.getContainerVethByNS(lxcName)
	if veth == "" {
		fmt.Printf("Warning: could not find veth for %s\n", lxcName)
		return
	}
	exec.Command("tc", "qdisc", "del", "dev", veth, "root").Run()
	exec.Command("tc", "qdisc", "del", "dev", veth, "ingress").Run()
	if downMbps > 0 {
		rate := fmt.Sprintf("%dmbit", downMbps)
		burst := fmt.Sprintf("%dkbit", downMbps*100)
		exec.Command("tc", "qdisc", "add", "dev", veth, "root", "handle", "1:", "htb", "default", "10").Run()
		exec.Command("tc", "class", "add", "dev", veth, "parent", "1:", "classid", "1:10", "htb", "rate", rate, "burst", burst).Run()
	}
	if upMbps > 0 {
		rate := fmt.Sprintf("%dmbit", upMbps)
		burst := fmt.Sprintf("%dkbit", upMbps*100)
		exec.Command("tc", "qdisc", "add", "dev", veth, "handle", "ffff:", "ingress").Run()
		exec.Command("tc", "filter", "add", "dev", veth, "parent", "ffff:", "protocol", "all", "u32", "match", "u32", "0", "0", "police", "rate", rate, "burst", burst, "drop", "flowid", ":1").Run()
	}
	fmt.Printf("Bandwidth limit: %s down=%d Mbps up=%d Mbps on %s\n", lxcName, downMbps, upMbps, veth)
}

func (m *Manager) cleanupBandwidthLimit(lxcName string) {
	veth := m.getContainerVethByNS(lxcName)
	if veth != "" {
		exec.Command("tc", "qdisc", "del", "dev", veth, "root").Run()
		exec.Command("tc", "qdisc", "del", "dev", veth, "ingress").Run()
	}
}

func (m *Manager) getContainerVethByNS(lxcName string) string {
	pid := m.getContainerInitPID(lxcName)
	if pid == "" {
		return ""
	}
	cmd := exec.Command("sh", "-c",
		fmt.Sprintf("nsenter -t %s -n ip -o link show 2>/dev/null | grep -oP 'eth0@if\\K[0-9]+'", pid))
	out, _ := cmd.Output()
	ifIdx := strings.TrimSpace(string(out))
	if ifIdx == "" {
		return ""
	}
	cmd2 := exec.Command("sh", "-c",
		fmt.Sprintf("ip -o link show | grep '^%s:' | grep -oP 'veth[^:@]+'", ifIdx))
	out2, _ := cmd2.Output()
	return strings.TrimSpace(string(out2))
}

// StopContainer stops an LXC container by its ID
func (m *Manager) StopContainer(id int) error {
	c := config.FindContainer(id)
	if c == nil {
		return fmt.Errorf("container not found: %d", id)
	}
	lxcName := c.LxcName()

	status, _ := m.GetContainerStatus(lxcName)
	if status != "running" {
		config.UpdateContainerStatus(id, "stopped")
		m.CleanPortMappings(id)
		CleanFirewallRules(id)
		m.cleanupBandwidthLimit(lxcName)
		return nil
	}

	m.CleanPortMappings(id)
	CleanFirewallRules(id)
	m.cleanupBandwidthLimit(lxcName)

	cmd := exec.Command("lxc-stop", "-n", lxcName)
	output, err := cmd.CombinedOutput()
	if err != nil {
		if strings.Contains(string(output), "not running") {
			config.UpdateContainerStatus(id, "stopped")
			return nil
		}
		return fmt.Errorf("failed to stop container: %v, output: %s", err, string(output))
	}

	config.UpdateContainerStatus(id, "stopped")
	fmt.Printf("Container %d (%s) stopped\n", id, c.Name)
	return nil
}

// RestartContainer restarts an LXC container by its ID
func (m *Manager) RestartContainer(id int) error {
	c := config.FindContainer(id)
	if c == nil {
		return fmt.Errorf("container not found: %d", id)
	}

	status, _ := m.GetContainerStatus(c.LxcName())
	if status == "running" {
		if err := m.StopContainer(id); err != nil {
			if !strings.Contains(err.Error(), "not running") {
				return err
			}
		}
		time.Sleep(1 * time.Second)
	}
	return m.StartContainer(id)
}

// EnsureContainerIPv4 brings eth0 up and asks the guest network stack for DHCP.
func (m *Manager) EnsureContainerIPv4(id int) (string, error) {
	c := config.FindContainer(id)
	if c == nil {
		return "", fmt.Errorf("container not found: %d", id)
	}
	lxcName := c.LxcName()
	status, _ := m.GetContainerStatus(lxcName)
	if status != "running" {
		return "", fmt.Errorf("container %d is not running; cannot configure IPv4", id)
	}

	if ip, err := m.GetContainerIP(lxcName); err == nil && ip != "" {
		c.IP = ip
		config.SaveConfig()
		return ip, nil
	}

	script := `
set +e
ip link set lo up 2>/dev/null
ip link set eth0 up 2>/dev/null

if command -v systemctl >/dev/null 2>&1; then
	systemctl start NetworkManager >/dev/null 2>&1
	systemctl start systemd-networkd >/dev/null 2>&1
	systemctl start networking >/dev/null 2>&1
fi
if command -v rc-service >/dev/null 2>&1; then
	rc-service networking start >/dev/null 2>&1
fi
if command -v nmcli >/dev/null 2>&1; then
	nmcli networking on >/dev/null 2>&1
	nmcli device set eth0 managed yes >/dev/null 2>&1
	nmcli connection up eth0 >/dev/null 2>&1 || nmcli device connect eth0 >/dev/null 2>&1
fi
if command -v dhclient >/dev/null 2>&1; then
	timeout 12 dhclient -4 -v eth0 >/dev/null 2>&1
elif command -v dhcpcd >/dev/null 2>&1; then
	pkill dhcpcd >/dev/null 2>&1 || true
	rm -f /run/dhcpcd*.pid /var/lib/dhcpcd/* /run/dhcpcd/* 2>/dev/null
	timeout 20 dhcpcd -4 -q -w -K eth0 >/dev/null 2>&1
elif command -v udhcpc >/dev/null 2>&1; then
	timeout 12 udhcpc -i eth0 -q >/dev/null 2>&1
elif command -v busybox >/dev/null 2>&1; then
	timeout 12 busybox udhcpc -i eth0 -q >/dev/null 2>&1
fi
ip -4 addr show eth0 2>/dev/null | awk '/inet / {sub(/\/.*/, "", $2); print $2; exit}'
`
	ctx, cancel := context.WithTimeout(context.Background(), 25*time.Second)
	defer cancel()
	cmd := exec.CommandContext(ctx, "lxc-attach", "-n", lxcName, "--", "sh", "-c", script)
	output, err := cmd.CombinedOutput()
	if ctx.Err() == context.DeadlineExceeded {
		return "", fmt.Errorf("timed out waiting for IPv4 DHCP in %s", lxcName)
	}
	if err != nil {
		return "", fmt.Errorf("failed to run IPv4 repair in %s: %v, output: %s", lxcName, err, string(output))
	}
	ip := strings.TrimSpace(string(output))
	if ip == "" {
		return "", fmt.Errorf("no IPv4 address after DHCP repair in %s", lxcName)
	}
	c.IP = ip
	config.SaveConfig()
	return ip, nil
}

// WarmSSH waits briefly for container networking metadata and then ensures sshd
// is installed, configured, running, and using the saved root password.
func (m *Manager) WarmSSH(id int) error {
	c := config.FindContainer(id)
	if c == nil {
		return fmt.Errorf("container not found: %d", id)
	}
	lxcName := c.LxcName()
	status, _ := m.GetContainerStatus(lxcName)
	if status != "running" {
		return fmt.Errorf("container %d is not running; cannot warm SSH", id)
	}

	for retry := 0; retry < 15; retry++ {
		if ip, err := m.GetContainerIP(lxcName); err == nil && ip != "" {
			if current := config.FindContainer(id); current != nil {
				current.IP = ip
				config.SaveConfig()
			}
			break
		}
		time.Sleep(2 * time.Second)
	}
	if current := config.FindContainer(id); current != nil && current.IP == "" {
		if ip, err := m.EnsureContainerIPv4(id); err == nil && ip != "" {
			current.IP = ip
			config.SaveConfig()
		}
	}

	return m.EnsureSSH(id)
}

func (m *Manager) WarmSSHAsync(id int, reason string) {
	if _, loaded := sshWarmupScheduled.LoadOrStore(id, struct{}{}); loaded {
		return
	}
	go func() {
		sshWarmupSem <- struct{}{}
		defer func() {
			<-sshWarmupSem
			sshWarmupScheduled.Delete(id)
		}()

		if err := m.WarmSSH(id); err != nil {
			if c := config.FindContainer(id); c != nil {
				fmt.Printf("Warning: SSH warmup failed for %s (%s, %s): %v\n", c.LxcName(), c.Name, reason, err)
			} else {
				fmt.Printf("Warning: SSH warmup failed for container %d (%s): %v\n", id, reason, err)
			}
		}
	}()
}

func sshEnsureLock(id int) *sync.Mutex {
	lock, _ := sshEnsureLocks.LoadOrStore(id, &sync.Mutex{})
	return lock.(*sync.Mutex)
}

// DestroyContainer destroys an LXC container by its ID
func (m *Manager) DestroyContainer(id int) error {
	if id <= 0 {
		return fmt.Errorf("invalid container id: %d", id)
	}
	c := config.FindContainer(id)
	if c == nil {
		return fmt.Errorf("container not found: %d", id)
	}
	lxcName := c.LxcName()
	if c.IPv6 != "" || len(c.IPv6Addresses) > 0 {
		c.NormalizeNetworkAssignments()
		for _, assignment := range c.IPv6Addresses {
			uplink := assignment.Interface
			if uplink == "" {
				uplink = c.IPv6Interface
			}
			if uplink != "" {
				removeHostIPv6Routing(assignment.Address, uplink)
			}
		}
	}

	if err := m.StopContainer(id); err != nil {
		return fmt.Errorf("failed to stop container before destroy: %v", err)
	}
	time.Sleep(1 * time.Second)

	containerDir := filepath.Join(m.LxcPath, lxcName)
	m.detachContainerMounts(containerDir)
	m.detachContainerLoopDevices(containerDir)

	// Retry lxc-destroy up to 3 times, since LXC may need time to release resources
	var destroyErr error
	for attempt := 0; attempt < 3; attempt++ {
		cmd := exec.Command("lxc-destroy", "-n", lxcName, "-f")
		output, err := cmd.CombinedOutput()
		if err == nil {
			destroyErr = nil
			break
		}
		out := string(output)
		if strings.Contains(strings.ToLower(out), "does not exist") ||
			strings.Contains(strings.ToLower(out), "not found") ||
			strings.Contains(strings.ToLower(out), "is not defined") {
			destroyErr = nil
			break
		}
		destroyErr = fmt.Errorf("failed to destroy container (attempt %d/3): %v, output: %s", attempt+1, err, out)
		if attempt < 2 {
			// Wait for LXC to release resources before retry
			time.Sleep(2 * time.Second)
			m.detachContainerMounts(containerDir)
			m.detachContainerLoopDevices(containerDir)
		}
	}

	if err := m.cleanupContainerStorage(lxcName); err != nil {
		if destroyErr != nil {
			return fmt.Errorf("%v; cleanup also failed: %v", destroyErr, err)
		}
		return err
	}
	if status, err := m.GetContainerStatus(lxcName); err == nil {
		if destroyErr != nil {
			return fmt.Errorf("%v; container still exists after cleanup with status %s", destroyErr, status)
		}
		return fmt.Errorf("container still exists after cleanup with status %s", status)
	}
	// Remove snapshot physical files (by container ID, not lxcName)
	snapshotDir := filepath.Join(snapshotBaseDir(), strconv.Itoa(id))
	if err := safePathUnder(snapshotDir, snapshotBaseDir()); err == nil {
		os.RemoveAll(snapshotDir)
	}

	// Also remove any legacy snapshot dir that used lxcName
	legacySnapshotDir := filepath.Join(snapshotBaseDir(), lxcName)
	if legacySnapshotDir != snapshotDir {
		if err := safePathUnder(legacySnapshotDir, snapshotBaseDir()); err == nil {
			os.RemoveAll(legacySnapshotDir)
		}
	}

	if !config.RemoveContainer(id) {
		return fmt.Errorf("container destroyed but config entry was not removed: %d", id)
	}
	if config.FindContainer(id) != nil {
		return fmt.Errorf("container destroyed but config entry still exists: %d", id)
	}
	fmt.Printf("Container %d destroyed\n", id)
	return nil
}

// EnsureSSH installs and starts sshd, enables root password login, and verifies port 22.
func (m *Manager) EnsureSSH(id int) error {
	lock := sshEnsureLock(id)
	lock.Lock()
	defer lock.Unlock()

	c := config.FindContainer(id)
	if c == nil {
		return fmt.Errorf("container not found: %d", id)
	}
	lxcName := c.LxcName()

	status, _ := m.GetContainerStatus(lxcName)
	if status != "running" {
		return fmt.Errorf("container %d is not running; cannot configure SSH", id)
	}

	if c.SSHPassword == "" {
		c.SSHPassword = generateRandomString(16)
		config.SaveConfig()
	}

	script := sshSetupScript(true, false) // keep pubkey enabled for runtime ensure

	ctx, cancel := context.WithTimeout(context.Background(), 180*time.Second)
	defer cancel()
	cmd := exec.CommandContext(ctx, "lxc-attach", "-n", lxcName, "--", "sh", "-c", script)
	output, err := cmd.CombinedOutput()
	if ctx.Err() == context.DeadlineExceeded {
		return fmt.Errorf("timed out configuring SSH in container %d after 180s; package manager or service startup may be stuck, output: %s", id, string(output))
	}
	if err != nil {
		return fmt.Errorf("failed to configure SSH in container %d: %v, output: %s", id, err, string(output))
	}
	if err := m.quickEnsureSSHPassword(lxcName, c.SSHPassword); err != nil {
		return fmt.Errorf("failed to set SSH password in container %d: %v", id, err)
	}

	if c.IP == "" {
		if ip, ipErr := m.GetContainerIP(lxcName); ipErr == nil && ip != "" {
			c.IP = ip
			config.SaveConfig()
		}
	}
	if c.IP != "" {
		if mapErr := m.ApplyPortMappings(id); mapErr != nil {
			fmt.Printf("Warning: failed to refresh SSH port mapping for %s: %v\n", lxcName, mapErr)
		}
	}

	fmt.Printf("SSH ready in container %d (root password login enabled)\n", id)
	return nil
}

func (m *Manager) quickEnsureSSHPassword(lxcName, password string) error {
	if err := validateRootPassword(password); err != nil {
		return err
	}
	ctx, cancel := context.WithTimeout(context.Background(), 8*time.Second)
	defer cancel()
	cmd := exec.CommandContext(ctx, "lxc-attach", "-n", lxcName, "--", "chpasswd")
	cmd.Stdin = strings.NewReader(rootPasswordInput(password))
	output, err := cmd.CombinedOutput()
	if err != nil {
		return fmt.Errorf("failed to update SSH password quickly: %v, output: %s", err, string(output))
	}
	return nil
}

func validateRootPassword(password string) error {
	if password == "" {
		return fmt.Errorf("empty SSH password")
	}
	if strings.ContainsAny(password, "\r\n") || strings.ContainsRune(password, '\x00') {
		return fmt.Errorf("SSH password contains unsupported control characters")
	}
	return nil
}

func rootPasswordInput(password string) string {
	return "root:" + password + "\n"
}

func (m *Manager) containerPortListening(lxcName string, port int) bool {
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	check := fmt.Sprintf("(ss -ltn 2>/dev/null || netstat -tln 2>/dev/null) | grep -Eq '(^|[[:space:]])[^[:space:]]*:%d[[:space:]]'", port)
	return exec.CommandContext(ctx, "lxc-attach", "-n", lxcName, "--", "sh", "-c", check).Run() == nil
}

func sshSetupScript(startService bool, disablePubkeyAuth bool) string {
	pubkeyValue := "yes"
	if disablePubkeyAuth {
		pubkeyValue = "no"
	}
	script := `set -u

# DNS setup: handle both traditional /etc/resolv.conf and systemd-resolved (Ubuntu 24.04).
# On modern distros, /etc/resolv.conf is a symlink managed by systemd-resolved.
# Remove any existing symlink and write a real file so DNS always works in LXC.
if [ -L /etc/resolv.conf ] 2>/dev/null; then
	rm -f /etc/resolv.conf
fi
# Also try resolvectl for systemd-resolved setups
if command -v resolvectl >/dev/null 2>&1; then
	resolvectl dns eth0 10.0.3.1 2>/dev/null || true
	resolvectl dns eth0 8.8.8.8 2>/dev/null || true
	resolvectl domain eth0 '~.' 2>/dev/null || true
fi
# Check if we have a usable (non-localhost) nameserver; if not, force-write one.
# Avoid the trap where systemd stub resolver puts "nameserver 127.0.0.53"
# but doesn't actually resolve anything.
if ! grep -q '^nameserver [1-9]' /etc/resolv.conf 2>/dev/null; then
	echo "nameserver 10.0.3.1" > /etc/resolv.conf
	echo "nameserver 8.8.8.8" >> /etc/resolv.conf
fi
export DEBIAN_FRONTEND=noninteractive
export APT_LISTCHANGES_FRONTEND=none

run_timeout() {
	if command -v timeout >/dev/null 2>&1; then
		timeout "$@"
	else
		shift
		"$@"
	fi
}

has_sshd() {
	command -v sshd >/dev/null 2>&1 || [ -x /usr/sbin/sshd ] || [ -x /sbin/sshd ]
}

sshd_path() {
	if command -v sshd >/dev/null 2>&1; then
		command -v sshd
	elif [ -x /usr/sbin/sshd ]; then
		printf /usr/sbin/sshd
	elif [ -x /sbin/sshd ]; then
		printf /sbin/sshd
	else
		return 1
	fi
}

install_sshd() {
	if has_sshd; then
		return 0
	fi
	if command -v apt-get >/dev/null 2>&1; then
		for i in 1 2; do
			run_timeout 20 dpkg --configure -a >/dev/null 2>&1 || true
			run_timeout 45 apt-get update -o Acquire::Retries=2 -o Acquire::http::Timeout=15 &&
				run_timeout 90 apt-get install -y --no-install-recommends -o Dpkg::Options::=--force-confold openssh-server passwd iproute2 procps net-tools &&
				return 0
			sleep 3
		done
	elif command -v apk >/dev/null 2>&1; then
		for i in 1 2 3; do
			run_timeout 120 apk add --no-cache openssh-server openssh-client shadow iproute2 procps net-tools && return 0
			sleep 3
		done
	elif command -v pacman >/dev/null 2>&1; then
		run_timeout 45 pacman -Syu --noconfirm >/dev/null 2>&1 || true
		run_timeout 90 pacman -S --noconfirm openssh shadow iproute2 procps-ng net-tools && return 0
	elif command -v dnf >/dev/null 2>&1; then
		run_timeout 90 dnf install -y openssh-server openssh-clients passwd iproute procps-ng net-tools && return 0
	elif command -v yum >/dev/null 2>&1; then
		run_timeout 90 yum install -y openssh-server openssh-clients passwd iproute procps-ng net-tools && return 0
	elif command -v nix-env >/dev/null 2>&1; then
		{ run_timeout 120 nix-env -iA nixos.openssh >/dev/null 2>&1 && return 0; } || true
		# NixOS cloud images may already have sshd or use different package paths
		# Fall through to let the script try whatever sshd is available
	fi
	return 1
}

ensure_sshd_runtime_dir() {
	mkdir -p /run/sshd /var/run/sshd
	chmod 0755 /run/sshd /var/run/sshd 2>/dev/null || true
}

set_sshd_option() {
	key="$1"
	value="$2"
	file=/etc/ssh/sshd_config
	tmp="${file}.clicd"
	touch "$file"
	awk -v key="$key" -v value="$value" '
		BEGIN { done=0; inmatch=0 }
		/^[[:space:]]*Match[[:space:]]/ {
			if (!done) { print key " " value; done=1 }
			inmatch=1
			print
			next
		}
		!inmatch && $0 ~ "^[#[:space:]]*" key "[[:space:]]+" {
			if (!done) { print key " " value; done=1 }
			next
		}
		{ print }
		END { if (!done) print key " " value }
	' "$file" >"$tmp" && cat "$tmp" >"$file"
	rm -f "$tmp"
}

install_sshd || exit 30

mkdir -p /etc/ssh /etc/ssh/sshd_config.d
ensure_sshd_runtime_dir
ssh-keygen -A >/dev/null 2>&1 || true

cat >/etc/ssh/sshd_config.d/99-clicd.conf <<'EOF'
PermitRootLogin yes
PubkeyAuthentication __CLICD_PUBKEY_AUTH__
PasswordAuthentication yes
KbdInteractiveAuthentication no
ChallengeResponseAuthentication no
UsePAM no
EOF

set_sshd_option PermitRootLogin yes
set_sshd_option PubkeyAuthentication __CLICD_PUBKEY_AUTH__
set_sshd_option PasswordAuthentication yes
set_sshd_option KbdInteractiveAuthentication no
set_sshd_option ChallengeResponseAuthentication no
set_sshd_option UsePAM no

if command -v rc-update >/dev/null 2>&1; then
	rc-update add sshd default >/dev/null 2>&1 || true
fi
if command -v systemctl >/dev/null 2>&1; then
	systemctl stop ssh.socket 2>/dev/null || true
	systemctl disable ssh.socket 2>/dev/null || true
	systemctl enable ssh >/dev/null 2>&1 || systemctl enable sshd >/dev/null 2>&1 || true
fi
if command -v update-rc.d >/dev/null 2>&1; then
	update-rc.d ssh defaults >/dev/null 2>&1 || true
fi
if command -v chkconfig >/dev/null 2>&1; then
	chkconfig sshd on >/dev/null 2>&1 || true
fi

SSHD_BIN="$(sshd_path)" || exit 32
ensure_sshd_runtime_dir
"$SSHD_BIN" -t -f /etc/ssh/sshd_config >/tmp/clicd-sshd-test.log 2>&1 || {
	cat /tmp/clicd-sshd-test.log
	exit 32
}
`
	script = strings.ReplaceAll(script, "__CLICD_PUBKEY_AUTH__", pubkeyValue)
	if !startService {
		return script
	}
	return script + `
# Disable socket-activated SSH (Ubuntu 24.04 default) to avoid conflicts.
if command -v systemctl >/dev/null 2>&1; then
	systemctl stop ssh.socket 2>/dev/null || true
	systemctl disable ssh.socket 2>/dev/null || true
fi
ensure_sshd_runtime_dir
if command -v systemctl >/dev/null 2>&1 && [ -d /run/systemd/system ]; then
	systemctl restart ssh >/dev/null 2>&1 || systemctl restart sshd >/dev/null 2>&1 || true
fi
service ssh restart >/dev/null 2>&1 ||
	service sshd restart >/dev/null 2>&1 ||
	rc-service sshd restart >/dev/null 2>&1 ||
	/etc/init.d/ssh restart >/dev/null 2>&1 ||
	/etc/init.d/sshd restart >/dev/null 2>&1 ||
	true

ensure_sshd_runtime_dir

for i in 1 2 3 4 5; do
	if (ss -ltn 2>/dev/null || netstat -tln 2>/dev/null) | grep -Eq '(^|[[:space:]])[^[:space:]]*:22[[:space:]]'; then
		exit 0
	fi
	if pgrep -x sshd >/dev/null 2>&1; then
		exit 0
	fi
	sleep 1
done

if ! (ss -ltn 2>/dev/null || netstat -tln 2>/dev/null) | grep -Eq '(^|[[:space:]])[^[:space:]]*:22[[:space:]]'; then
	pkill -x sshd >/dev/null 2>&1 || killall sshd >/dev/null 2>&1 || true
	rm -f /run/sshd.pid /var/run/sshd.pid
	ensure_sshd_runtime_dir
	"$SSHD_BIN" -f /etc/ssh/sshd_config >/dev/null 2>&1 || exit 32
fi

for i in 1 2 3 4 5; do
	if (ss -ltn 2>/dev/null || netstat -tln 2>/dev/null) | grep -Eq '(^|[[:space:]])[^[:space:]]*:22[[:space:]]'; then
		exit 0
	fi
	sleep 1
done
pgrep -x sshd >/dev/null 2>&1 || exit 33
`
}

// ResetSSHPassword resets the root password of a container
func (m *Manager) ResetSSHPassword(id int, password string) (string, error) {
	c := config.FindContainer(id)
	if c == nil {
		return "", fmt.Errorf("container not found: %d", id)
	}
	lxcName := c.LxcName()

	newPassword := strings.TrimSpace(password)
	if newPassword == "" {
		newPassword = generateRandomString(16)
	}

	if c.Status == "running" {
		c.SSHPassword = newPassword
		config.SaveConfig()
		if err := m.EnsureSSH(id); err != nil {
			return "", err
		}
	} else {
		if err := m.ensureDiskImageMounted(lxcName); err != nil {
			return "", err
		}
		rootfsPath := filepath.Join(m.LxcPath, lxcName, "rootfs")
		if err := m.preconfigureSSH(rootfsPath, c.Template, ""); err != nil {
			return "", fmt.Errorf("failed to configure SSH: %v", err)
		}
		if err := m.setRootfsPassword(rootfsPath, newPassword); err != nil {
			return "", fmt.Errorf("failed to set password: %v", err)
		}
		c.SSHPassword = newPassword
		config.SaveConfig()
	}

	return newPassword, nil
}

func (m *Manager) rootfsCommand(rootfsPath string, args ...string) (*exec.Cmd, error) {
	cleanRootfsPath, err := m.safeRootfsPath(rootfsPath)
	if err != nil {
		return nil, err
	}
	safeArgs, err := safeRootfsCommandArgs(args)
	if err != nil {
		return nil, err
	}

	marker := filepath.Join(cleanRootfsPath, ".clicd-unprivileged-shifted")
	if _, err := os.Stat(marker); err == nil {
		uidBase, gidBase, mapErr := unprivilegedIDMap()
		if mapErr == nil {
			cmdArgs := []string{
				"-m", fmt.Sprintf("u:0:%d:65536", uidBase),
				"-m", fmt.Sprintf("g:0:%d:65536", gidBase),
				"--", "chroot", "--", cleanRootfsPath,
			}
			cmdArgs = append(cmdArgs, safeArgs...)
			return exec.Command("lxc-usernsexec", cmdArgs...), nil
		}
	}
	cmdArgs := append([]string{"--", cleanRootfsPath}, safeArgs...)
	return exec.Command("chroot", cmdArgs...), nil
}

func (m *Manager) runRootfsCommand(rootfsPath string, args ...string) error {
	cmd, err := m.rootfsCommand(rootfsPath, args...)
	if err != nil {
		return err
	}
	return cmd.Run()
}

func (m *Manager) setRootfsPassword(rootfsPath, password string) error {
	if err := validateRootPassword(password); err != nil {
		return err
	}
	cmd, err := m.rootfsCommand(rootfsPath, "chpasswd")
	if err != nil {
		return err
	}
	cmd.Stdin = strings.NewReader(rootPasswordInput(password))
	output, err := cmd.CombinedOutput()
	if err != nil {
		return fmt.Errorf("%v, output: %s", err, string(output))
	}
	return nil
}

func (m *Manager) installRootAuthorizedKey(rootfsPath, publicKey string) error {
	key, err := NormalizeSSHPublicKey(publicKey)
	if err != nil {
		return err
	}
	if key == "" {
		return nil
	}
	sshDir := filepath.Join(rootfsPath, "root", ".ssh")
	if err := os.MkdirAll(sshDir, 0700); err != nil {
		return err
	}
	authPath := filepath.Join(sshDir, "authorized_keys")
	existing, _ := os.ReadFile(authPath)
	lines := strings.Split(string(existing), "\n")
	for _, line := range lines {
		if strings.TrimSpace(line) == key {
			_ = os.Chmod(sshDir, 0700)
			_ = os.Chmod(authPath, 0600)
			_ = os.Chown(sshDir, 0, 0)
			_ = os.Chown(authPath, 0, 0)
			return nil
		}
	}
	content := strings.TrimRight(string(existing), "\r\n")
	if content != "" {
		content += "\n"
	}
	content += key + "\n"
	if err := os.WriteFile(authPath, []byte(content), 0600); err != nil {
		return err
	}
	_ = os.Chmod(sshDir, 0700)
	_ = os.Chmod(authPath, 0600)
	_ = os.Chown(sshDir, 0, 0)
	_ = os.Chown(authPath, 0, 0)
	return nil
}

func safeRootfsCommandArgs(args []string) ([]string, error) {
	if len(args) == 0 {
		return nil, fmt.Errorf("empty rootfs command")
	}
	allowed := map[string]bool{
		"chpasswd":  true,
		"rc-update": true,
		"sh":        true,
		"systemctl": true,
	}
	if !allowed[args[0]] || strings.HasPrefix(args[0], "-") || strings.Contains(args[0], "/") {
		return nil, fmt.Errorf("rootfs command is not allowed: %s", args[0])
	}
	for _, arg := range args {
		if strings.ContainsRune(arg, '\x00') {
			return nil, fmt.Errorf("rootfs command argument contains NUL byte")
		}
	}
	if args[0] == "sh" {
		if len(args) != 3 || args[1] != "-c" {
			return nil, fmt.Errorf("unsupported rootfs shell invocation")
		}
		if !isCLICDManagedRootfsScript(args[2]) {
			return nil, fmt.Errorf("refusing unmanaged rootfs shell script")
		}
	}
	return append([]string(nil), args...), nil
}

func isCLICDManagedRootfsScript(script string) bool {
	return strings.Contains(script, "99-clicd.conf") &&
		strings.Contains(script, "install_sshd") &&
		!strings.Contains(script, "ROOT_PASSWORD") &&
		!strings.Contains(script, "chpasswd")
}

func (m *Manager) safeRootfsPath(rootfsPath string) (string, error) {
	if rootfsPath == "" {
		return "", fmt.Errorf("empty rootfs path")
	}
	if !filepath.IsAbs(rootfsPath) {
		return "", fmt.Errorf("rootfs path must be absolute: %s", rootfsPath)
	}

	cleanRootfsPath := filepath.Clean(rootfsPath)
	cleanLxcPath, err := filepath.Abs(m.LxcPath)
	if err != nil {
		return "", fmt.Errorf("failed to resolve LXC path: %v", err)
	}
	cleanLxcPath = filepath.Clean(cleanLxcPath)

	if cleanRootfsPath == cleanLxcPath {
		return "", fmt.Errorf("refusing LXC base path as rootfs: %s", cleanRootfsPath)
	}
	if filepath.Base(cleanRootfsPath) != "rootfs" {
		return "", fmt.Errorf("refusing non-rootfs path: %s", cleanRootfsPath)
	}
	if filepath.Dir(cleanRootfsPath) == cleanLxcPath {
		return "", fmt.Errorf("refusing rootfs directly under LXC path: %s", cleanRootfsPath)
	}

	rel, err := filepath.Rel(cleanLxcPath, cleanRootfsPath)
	if err != nil {
		return "", fmt.Errorf("failed to validate rootfs path: %v", err)
	}
	if rel == "." || rel == ".." || strings.HasPrefix(rel, ".."+string(os.PathSeparator)) || filepath.IsAbs(rel) {
		return "", fmt.Errorf("refusing unsafe rootfs path: %s", cleanRootfsPath)
	}
	parts := strings.Split(rel, string(os.PathSeparator))
	if len(parts) != 2 || parts[1] != "rootfs" {
		return "", fmt.Errorf("refusing nested or malformed rootfs path: %s", cleanRootfsPath)
	}
	if strings.HasPrefix(parts[0], "-") || !regexp.MustCompile(`^[A-Za-z0-9_.-]+$`).MatchString(parts[0]) {
		return "", fmt.Errorf("refusing unsafe container directory name: %s", parts[0])
	}
	return cleanRootfsPath, nil
}

func (m *Manager) cleanupContainerStorage(lxcName string) error {
	containerDir := filepath.Join(m.LxcPath, lxcName)
	cleanPath, err := filepath.Abs(containerDir)
	if err != nil {
		return fmt.Errorf("failed to resolve container path: %v", err)
	}
	basePath, err := filepath.Abs(m.LxcPath)
	if err != nil {
		return fmt.Errorf("failed to resolve LXC path: %v", err)
	}
	if cleanPath == basePath || !strings.HasPrefix(cleanPath, basePath+string(os.PathSeparator)) {
		return fmt.Errorf("refusing to remove unsafe container path: %s", cleanPath)
	}
	if _, err := os.Stat(cleanPath); os.IsNotExist(err) {
		return nil
	}
	exec.Command("lxc-stop", "-n", lxcName, "-k").Run()
	exec.Command("lxc-destroy", "-n", lxcName, "-f").Run()
	m.detachContainerMounts(cleanPath)
	m.detachContainerLoopDevices(cleanPath)
	rootfs := filepath.Join(cleanPath, "rootfs")
	exec.Command("umount", "-R", "-l", rootfs).Run()
	m.detachContainerMounts(cleanPath)
	m.detachContainerLoopDevices(cleanPath)
	if err := os.RemoveAll(cleanPath); err != nil {
		return fmt.Errorf("failed to remove container directory %s: %v", cleanPath, err)
	}
	return nil
}

func (m *Manager) detachContainerMounts(containerDir string) {
	out, err := exec.Command("findmnt", "-R", "-n", "-o", "TARGET", containerDir).Output()
	if err != nil {
		return
	}
	lines := strings.Split(strings.TrimSpace(string(out)), "\n")
	for i, j := 0, len(lines)-1; i < j; i, j = i+1, j-1 {
		lines[i], lines[j] = lines[j], lines[i]
	}
	for _, line := range lines {
		target := strings.TrimSpace(line)
		if target == "" {
			continue
		}
		exec.Command("umount", "-R", "-l", target).Run()
	}
}

func (m *Manager) detachContainerLoopDevices(containerDir string) {
	out, err := exec.Command("losetup", "-j", filepath.Join(containerDir, "rootfs.img")).Output()
	if err != nil {
		return
	}
	for _, line := range strings.Split(string(out), "\n") {
		line = strings.TrimSpace(line)
		if line == "" {
			continue
		}
		device := strings.TrimSuffix(strings.SplitN(line, ":", 2)[0], ":")
		if device != "" {
			exec.Command("losetup", "-d", device).Run()
		}
	}
}

// GetContainerStatus gets container running status
func (m *Manager) GetContainerStatus(lxcName string) (string, error) {
	cmd := exec.Command("lxc-info", "-n", lxcName, "-sH")
	output, err := cmd.Output()
	if err != nil {
		cmd2 := exec.Command("lxc-info", "-n", lxcName, "-s")
		output2, err2 := cmd2.Output()
		if err2 != nil {
			return "unknown", err2
		}
		output = output2
	}

	status := strings.TrimSpace(string(output))
	upper := strings.ToUpper(status)
	if strings.Contains(upper, "RUNNING") {
		return "running", nil
	}
	return "stopped", nil
}

// GetContainerIP gets container IP address
func (m *Manager) GetContainerIP(lxcName string) (string, error) {
	cmd := exec.Command("lxc-info", "-n", lxcName, "-iH")
	output, err := cmd.Output()
	if err != nil {
		cmd2 := exec.Command("lxc-info", "-n", lxcName, "-i")
		output2, err2 := cmd2.Output()
		if err2 != nil {
			return "", err2
		}
		output = output2
	}

	ip := strings.TrimSpace(string(output))
	// Always prefer IPv4; IPv6 addresses break WebSSH and port forwarding.
	re := regexp.MustCompile(`(\d+\.\d+\.\d+\.\d+)`)
	matches := re.FindStringSubmatch(ip)
	if len(matches) > 1 {
		return matches[1], nil
	}
	// If no IPv4 found, try lxc-attach as fallback (DHCP may be delayed)
	attachCmd := exec.Command("lxc-attach", "-n", lxcName, "--", "sh", "-c", "ip -4 addr show eth0 2>/dev/null | grep -oP 'inet \\K[\\d.]+' || true")
	if attachOut, attachErr := attachCmd.Output(); attachErr == nil {
		v4 := strings.TrimSpace(string(attachOut))
		if v4 != "" {
			return v4, nil
		}
	}
	return "", fmt.Errorf("no IPv4 address found for %s (IPv6 is disabled for containers)", lxcName)
}

// ListContainers lists all LXC containers and updates statuses
func (m *Manager) ListContainers() ([]config.Container, error) {
	containers := config.AppConfig.Containers
	for i := range containers {
		if containers[i].IsKVM() {
			continue
		}
		status, err := m.GetContainerStatus(containers[i].LxcName())
		if err == nil {
			containers[i].Status = status
		}
		if status == "running" {
			ip, err := m.GetContainerIP(containers[i].LxcName())
			if err == nil {
				containers[i].IP = ip
			}
		}
	}
	return containers, nil
}

// ImportExistingClicdContainers imports existing LXC containers into the CLICD
// config. Native CLICD containers keep ct-{id}; arbitrary LXC names are stored
// in Container.LXCName so Web and CLI can manage the same imported container.
func (m *Manager) ImportExistingClicdContainers() ([]config.Container, error) {
	entries, err := os.ReadDir(m.LxcPath)
	if err != nil {
		return nil, err
	}

	existingIDs := make(map[int]bool)
	existingNames := make(map[string]bool)
	existingLXCNames := make(map[string]bool)
	maxID := config.AppConfig.NextContainerID - 1
	for _, c := range config.AppConfig.Containers {
		existingIDs[c.ID] = true
		existingNames[c.Name] = true
		existingLXCNames[c.LxcName()] = true
		if c.ID > maxID {
			maxID = c.ID
		}
	}

	re := regexp.MustCompile(`^ct-([0-9]+)$`)
	imported := make([]config.Container, 0)
	for _, entry := range entries {
		if !entry.IsDir() {
			continue
		}
		lxcName := entry.Name()
		if existingLXCNames[lxcName] {
			continue
		}

		id := 0
		if matches := re.FindStringSubmatch(lxcName); len(matches) == 2 {
			if parsed, err := strconv.Atoi(matches[1]); err == nil && parsed > 0 && !existingIDs[parsed] {
				id = parsed
			}
		}
		if id == 0 {
			id = maxID + 1
			for existingIDs[id] {
				id++
			}
		}

		name := lxcName
		if existingNames[name] {
			name = fmt.Sprintf("imported-%d", id)
		}

		status, err := m.GetContainerStatus(lxcName)
		if err != nil || status == "" {
			status = "unknown"
		}

		c := config.Container{
			ID:               id,
			UUID:             config.NewContainerUUID(),
			Name:             name,
			LXCName:          lxcName,
			Virtualization:   config.VirtualizationLXC,
			Template:         "imported",
			VCPU:             1,
			RAMMB:            512,
			DiskGB:           10,
			NetworkBWMbps:    100,
			NetworkDownMbps:  100,
			NetworkUpMbps:    100,
			MonthlyTrafficGB: 1000,
			TrafficMode:      "total",
			Status:           status,
			CreatedAt:        time.Now().Format(time.RFC3339),
			PortMappingLimit: 2,
			SnapshotLimit:    config.DefaultSnapshotLimit,
		}

		if status == "running" {
			if ip, err := m.GetContainerIP(lxcName); err == nil {
				c.IP = ip
			}
		}

		config.AppConfig.Containers = append(config.AppConfig.Containers, c)
		imported = append(imported, c)
		existingIDs[id] = true
		existingNames[name] = true
		existingLXCNames[lxcName] = true
		if id > maxID {
			maxID = id
		}
	}

	if len(imported) > 0 {
		config.AppConfig.NextContainerID = maxID + 1
		if err := config.SaveConfig(); err != nil {
			return nil, err
		}
	}

	return imported, nil
}

func (m *Manager) replaceRootfsFromTemplate(lxcName string, tmpl *Template) error {
	if tmpl == nil {
		return fmt.Errorf("template is nil")
	}
	tmpName := fmt.Sprintf("clicd-reinstall-%s-%s", lxcName, generateRandomString(8))
	tmpDir := filepath.Join(m.LxcPath, tmpName)
	if err := os.RemoveAll(tmpDir); err != nil {
		return fmt.Errorf("failed to clean temporary reinstall directory: %v", err)
	}
	defer m.cleanupTemporaryContainer(tmpName)

	args := []string{
		"-n", tmpName,
		"-t", "download",
		"--",
		"-d", tmpl.Distro,
		"-r", tmpl.Release,
		"-a", tmpl.Arch,
	}
	if tmpl.Variant != "" {
		args = append(args, "--variant", tmpl.Variant)
	}
	output, err := exec.Command("lxc-create", args...).CombinedOutput()
	if err != nil {
		return fmt.Errorf("failed to download replacement rootfs: %v, output: %s", err, string(output))
	}

	tmpRootfs := filepath.Join(tmpDir, "rootfs")
	if !rootfsHasInit(tmpRootfs) {
		return fmt.Errorf("downloaded replacement rootfs is invalid: init not found")
	}

	rootfsPath := filepath.Join(m.LxcPath, lxcName, "rootfs")
	if err := m.ensureDiskImageMounted(lxcName); err != nil {
		return err
	}
	m.unmountRootfsChildMounts(rootfsPath)
	if err := os.MkdirAll(rootfsPath, 0755); err != nil {
		return err
	}
	if err := removeDirectoryContents(rootfsPath); err != nil {
		return fmt.Errorf("failed to clear old rootfs: %v", err)
	}
	if err := copyRootfsContents(tmpRootfs, rootfsPath); err != nil {
		return err
	}
	if !rootfsHasInit(rootfsPath) {
		return fmt.Errorf("replacement rootfs copy failed: init not found")
	}
	return nil
}

func (m *Manager) cleanupTemporaryContainer(lxcName string) {
	exec.Command("lxc-stop", "-n", lxcName, "-k").Run()
	exec.Command("lxc-destroy", "-n", lxcName, "-f").Run()
	os.RemoveAll(filepath.Join(m.LxcPath, lxcName))
}

func removeDirectoryContents(dir string) error {
	entries, err := os.ReadDir(dir)
	if err != nil {
		return err
	}
	for _, entry := range entries {
		if err := os.RemoveAll(filepath.Join(dir, entry.Name())); err != nil {
			return err
		}
	}
	return nil
}

func copyRootfsContents(src, dst string) error {
	output, err := exec.Command("cp", "-a", src+string(os.PathSeparator)+".", dst+string(os.PathSeparator)).CombinedOutput()
	if err != nil {
		return fmt.Errorf("failed to copy replacement rootfs: %v, output: %s", err, string(output))
	}
	return nil
}

// ReinstallContainer reinstalls the container OS
func (m *Manager) ReinstallContainer(id int, templateID string, authConfig ...ContainerConfig) error {
	c := config.FindContainer(id)
	if c == nil {
		return fmt.Errorf("container not found: %d", id)
	}

	tmpl := FindTemplate(templateID)
	if tmpl == nil {
		return fmt.Errorf("template not found: %s", templateID)
	}
	authCfg := ContainerConfig{SSHAuthMode: SSHAuthKeep}
	if len(authConfig) > 0 {
		authCfg = authConfig[0]
	}
	sshAccess, err := ResolveReinstallSSHAccess(c.SSHPassword, authCfg)
	if err != nil {
		return err
	}

	lxcName := c.LxcName()

	// Stop the container first
	status, _ := m.GetContainerStatus(lxcName)
	if status == "running" {
		m.StopContainer(id)
	}

	// Clean port mappings temporarily
	m.CleanPortMappings(id)
	CleanFirewallRules(id)

	// Download the new OS into a temporary container, then replace only the
	// existing rootfs. The target container directory and config are preserved.
	if err := m.replaceRootfsFromTemplate(lxcName, tmpl); err != nil {
		return err
	}

	if err := m.applyDiskLimit(lxcName, c.DiskGB); err != nil {
		return err
	}

	// Re-apply resource limits and mandatory security hardening.
	cfg := ContainerConfig{
		Name:             c.Name,
		TemplateID:       templateID,
		VCPU:             c.VCPU,
		RAMMB:            c.RAMMB,
		DiskGB:           c.DiskGB,
		NetworkBWMbps:    c.NetworkBWMbps,
		NetworkDownMbps:  c.NetworkDownMbps,
		NetworkUpMbps:    c.NetworkUpMbps,
		MonthlyTrafficGB: c.MonthlyTrafficGB,
		IOSpeedMBps:      c.IOSpeedMBps,
		IOReadMBps:       c.IOReadMBps,
		IOWriteMBps:      c.IOWriteMBps,
		AssignIPv6:       c.IPv6 != "" || len(c.IPv6Addresses) > 0,
		ExpiresAt:        c.ExpiresAt,
	}
	if err := m.applyResourceLimits(lxcName, cfg); err != nil {
		return err
	}
	if c.IPv6 != "" || len(c.IPv6Addresses) > 0 {
		c.NormalizeNetworkAssignments()
		if err := m.applyIPv6Config(lxcName, c.IPv6AddressStrings()...); err != nil {
			return err
		}
	}

	// Set root password and pre-configure network/SSH via chroot.
	rootfsPath := filepath.Join(m.LxcPath, lxcName, "rootfs")
	m.preconfigureNetwork(rootfsPath, templateID)
	if c.IPv6 != "" || len(c.IPv6Addresses) > 0 {
		if err := installContainerIPv6Init(rootfsPath, c.IPv6AddressStrings()...); err != nil {
			fmt.Printf("Warning: failed to install IPv6 init in %s after reinstall: %v\n", lxcName, err)
		}
	}
	c.SSHPassword = sshAccess.Password
	if err := m.preconfigureSSH(rootfsPath, templateID, sshAccess.Mode); err != nil {
		fmt.Printf("Warning: failed to pre-configure SSH in %s after reinstall: %v\n", lxcName, err)
	}
	if sshAccess.PublicKey != "" {
		if err := m.installRootAuthorizedKey(rootfsPath, sshAccess.PublicKey); err != nil {
			return fmt.Errorf("failed to install SSH public key: %v", err)
		}
	}
	if err := m.shiftRootfsForUnprivileged(lxcName); err != nil {
		return err
	}
	if err := m.setRootfsPassword(rootfsPath, c.SSHPassword); err != nil {
		fmt.Printf("Warning: failed to set root password in %s after reinstall: %v\n", lxcName, err)
	}

	// Update template and keep everything else the same
	c.Template = templateID
	c.SSHHostKey = ""
	c.Status = "running"
	config.SaveConfig()

	// Start the container to trigger ensureSSH
	if err := m.ensureDiskImageMounted(lxcName); err != nil {
		c.Status = "stopped"
		config.SaveConfig()
		return err
	}
	logFile, consoleLog, output, err := m.startLXCContainerDaemon(lxcName)
	if err != nil {
		fmt.Printf("Warning: failed to start container after reinstall: %v\n", err)
		c.Status = "stopped"
		config.SaveConfig()
		return fmt.Errorf("reinstalled but failed to start: %v, output: %s, lxc log: %s, console: %s", err, string(output), tailFile(logFile, 80), tailFile(consoleLog, 80))
	}
	if err := m.waitForLXCStartup(lxcName, logFile, consoleLog); err != nil {
		c.Status = "stopped"
		config.SaveConfig()
		return fmt.Errorf("reinstalled but container did not stay running: %v", err)
	}

	// Wait for network and install SSH
	time.Sleep(5 * time.Second)
	var ip string
	for retry := 0; retry < 5; retry++ {
		ip, _ = m.GetContainerIP(lxcName)
		if ip != "" {
			break
		}
		time.Sleep(2 * time.Second)
	}
	if ip != "" {
		c.IP = ip
		config.SaveConfig()
	}
	if ip != "" {
		if err := m.EnsureSSH(id); err != nil {
			return err
		}
	}
	// Apply bandwidth limit after reinstall
	config.NormalizeContainerResourceAliases(c)
	m.applyBandwidthLimit(c.LxcName(), c.NetworkDownMbps, c.NetworkUpMbps)
	if c.IPv6 != "" || len(c.IPv6Addresses) > 0 {
		if err := m.ApplyIPv6(id); err != nil {
			fmt.Printf("Warning: failed to apply IPv6 after reinstall: %v\n", err)
		}
	}

	fmt.Printf("Container %d (%s) reinstalled with %s\n", id, c.Name, templateID)
	return nil
}

// GetResourceUsage returns resource usage info for a container by ID.
// Rates come from the background monitor goroutine (no shared-state races).
func (m *Manager) GetResourceUsage(id int) (map[string]interface{}, error) {
	c := config.FindContainer(id)
	if c == nil {
		return nil, fmt.Errorf("container not found: %d", id)
	}
	lxcName := c.LxcName()
	usage := make(map[string]interface{})

	// Read raw values
	load1, load5, load15 := m.getContainerLoadAvg(lxcName)
	usage["load1"] = load1
	usage["load5"] = load5
	usage["load15"] = load15

	memUsage := readIntCommand(fmt.Sprintf(
		"cat /sys/fs/cgroup/lxc/%[1]s/memory.current 2>/dev/null || "+
			"cat /sys/fs/cgroup/lxc.payload.%[1]s/memory.current 2>/dev/null || "+
			"cat /sys/fs/cgroup/memory/lxc/%[1]s/memory.usage_in_bytes 2>/dev/null || echo 0", shellQuote(lxcName)))
	usage["memory_usage_bytes"] = memUsage

	cpuUsec := uint64(readIntCommand(fmt.Sprintf(
		"(cat /sys/fs/cgroup/lxc/%[1]s/cpu.stat 2>/dev/null || "+
			"cat /sys/fs/cgroup/lxc.payload.%[1]s/cpu.stat 2>/dev/null) | "+
			"awk '/usage_usec/ {print $2; found=1} END {if (!found) print 0}'", shellQuote(lxcName))))
	usage["cpu_usage_usec"] = cpuUsec

	diskUsage := readIntCommand(fmt.Sprintf("du -s -B1 %s 2>/dev/null | awk '{print $1}' || echo 0",
		shellQuote(filepath.Join(m.LxcPath, lxcName, "rootfs"))))
	usage["disk_usage_bytes"] = diskUsage

	rxBytes, txBytes := m.getContainerNetworkBytes(lxcName)
	usage["network_rx_bytes"] = rxBytes
	usage["network_tx_bytes"] = txBytes

	readBytes, writeBytes := m.getContainerDiskIOBytes(lxcName)
	usage["disk_read_bytes"] = readBytes
	usage["disk_write_bytes"] = writeBytes

	// Read rates from background monitor cache (single writer, no race)
	usageMu.RLock()
	rate, hasRate := rateCache[lxcName]
	usageMu.RUnlock()

	if hasRate && time.Since(rate.UpdatedAt) < 15*time.Second {
		usage["cpu_usage_pct"] = rate.CPUPct
		usage["network_rx_bps"] = rate.RXBps
		usage["network_tx_bps"] = rate.TXBps
		usage["disk_read_bps"] = rate.ReadBps
		usage["disk_write_bps"] = rate.WriteBps
	} else {
		// Cache miss or stale — return zeros (monitor will populate soon)
		usage["cpu_usage_pct"] = 0.0
		usage["network_rx_bps"] = 0.0
		usage["network_tx_bps"] = 0.0
		usage["disk_read_bps"] = 0.0
		usage["disk_write_bps"] = 0.0
	}

	return usage, nil
}

func (m *Manager) getContainerLoadAvg(lxcName string) (float64, float64, float64) {
	pid := m.getContainerInitPID(lxcName)
	if pid == "" {
		return 0, 0, 0
	}
	out, err := exec.Command("nsenter", "-t", pid, "-m", "-p", "cat", "/proc/loadavg").Output()
	if err != nil {
		return 0, 0, 0
	}
	parts := strings.Fields(string(out))
	if len(parts) < 3 {
		return 0, 0, 0
	}
	load1, _ := strconv.ParseFloat(parts[0], 64)
	load5, _ := strconv.ParseFloat(parts[1], 64)
	load15, _ := strconv.ParseFloat(parts[2], 64)
	return load1, load5, load15
}

func (m *Manager) getContainerNetworkBytes(lxcName string) (uint64, uint64) {
	pid := m.getContainerInitPID(lxcName)
	if pid == "" {
		return 0, 0
	}
	return readProcNetDev(fmt.Sprintf("/proc/%s/net/dev", pid))
}

// readProcNetDev parses /proc/PID/net/dev directly (no shell/awk fork).
func readProcNetDev(path string) (uint64, uint64) {
	data, err := os.ReadFile(path)
	if err != nil {
		return 0, 0
	}
	var rx, tx uint64
	scanner := bufio.NewScanner(strings.NewReader(string(data)))
	for scanner.Scan() {
		line := scanner.Text()
		// Skip header lines
		if strings.Contains(line, "|") || strings.Contains(line, "face") || strings.TrimSpace(line) == "" {
			continue
		}
		fields := strings.Fields(line)
		// Fields: face | rx_bytes rx_packets rx_errs rx_drop rx_fifo rx_frame rx_compressed rx_multicast | tx_bytes tx_packets tx_errs tx_drop tx_fifo tx_colls tx_carrier tx_compressed
		// Skip loopback (face starts with "lo")
		if len(fields) < 10 {
			continue
		}
		if strings.HasPrefix(fields[0], "lo") {
			continue
		}
		r, _ := strconv.ParseUint(fields[1], 10, 64)
		t, _ := strconv.ParseUint(fields[9], 10, 64)
		rx += r
		tx += t
	}
	return rx, tx
}

func (m *Manager) getContainerDiskIOBytes(lxcName string) (uint64, uint64) {
	pid := m.getContainerInitPID(lxcName)
	if pid == "" {
		return 0, 0
	}
	data, err := os.ReadFile(fmt.Sprintf("/proc/%s/io", pid))
	if err != nil {
		return 0, 0
	}
	var readBytes, writeBytes uint64
	scanner := bufio.NewScanner(strings.NewReader(string(data)))
	for scanner.Scan() {
		line := scanner.Text()
		if strings.HasPrefix(line, "read_bytes:") {
			val := strings.TrimSpace(strings.TrimPrefix(line, "read_bytes:"))
			readBytes, _ = strconv.ParseUint(val, 10, 64)
		} else if strings.HasPrefix(line, "write_bytes:") {
			val := strings.TrimSpace(strings.TrimPrefix(line, "write_bytes:"))
			writeBytes, _ = strconv.ParseUint(val, 10, 64)
		}
	}
	return readBytes, writeBytes
}

func (m *Manager) getContainerInitPID(lxcName string) string {
	cmd := exec.Command("lxc-info", "-n", lxcName, "-pH")
	out, err := cmd.Output()
	if err != nil {
		return ""
	}
	return strings.TrimSpace(string(out))
}

// readCgroupFile tries each path template in order, reads the file directly (no shell),
// and returns the first valid int64 value.
func readCgroupFile(name string, paths ...string) int64 {
	for _, tmpl := range paths {
		data, err := os.ReadFile(fmt.Sprintf(tmpl, name))
		if err != nil {
			continue
		}
		val, err := strconv.ParseInt(strings.TrimSpace(string(data)), 10, 64)
		if err == nil && val > 0 {
			return val
		}
	}
	return 0
}

// readCgroupCPUUsec tries each path template, reads cpu.stat, and extracts usage_usec.
func readCgroupCPUUsec(name string, paths ...string) uint64 {
	for _, tmpl := range paths {
		data, err := os.ReadFile(fmt.Sprintf(tmpl, name))
		if err != nil {
			continue
		}
		scanner := bufio.NewScanner(strings.NewReader(string(data)))
		for scanner.Scan() {
			line := scanner.Text()
			if strings.HasPrefix(line, "usage_usec ") {
				val, err := strconv.ParseUint(strings.TrimSpace(strings.TrimPrefix(line, "usage_usec")), 10, 64)
				if err == nil {
					return val
				}
			}
		}
	}
	return 0
}

// getNetworkBytesForPID reads /proc/PID/net/dev for a given PID (no lxc-info needed).
func getNetworkBytesForPID(pid string) (uint64, uint64) {
	if pid == "" {
		return 0, 0
	}
	return readProcNetDev(fmt.Sprintf("/proc/%s/net/dev", pid))
}

// getDiskIOBytesForPID reads /proc/PID/io for a given PID (no lxc-info needed).
func getDiskIOBytesForPID(pid string) (uint64, uint64) {
	if pid == "" {
		return 0, 0
	}
	data, err := os.ReadFile(fmt.Sprintf("/proc/%s/io", pid))
	if err != nil {
		return 0, 0
	}
	var readBytes, writeBytes uint64
	scanner := bufio.NewScanner(strings.NewReader(string(data)))
	for scanner.Scan() {
		line := scanner.Text()
		if strings.HasPrefix(line, "read_bytes:") {
			readBytes, _ = strconv.ParseUint(strings.TrimSpace(strings.TrimPrefix(line, "read_bytes:")), 10, 64)
		} else if strings.HasPrefix(line, "write_bytes:") {
			writeBytes, _ = strconv.ParseUint(strings.TrimSpace(strings.TrimPrefix(line, "write_bytes:")), 10, 64)
		}
	}
	return readBytes, writeBytes
}

// getContainerUptimeSeconds returns how long the container has been running (in seconds).
func (m *Manager) getContainerUptimeSeconds(lxcName string) float64 {
	pid := m.getContainerInitPID(lxcName)
	if pid == "" {
		return 0
	}
	// Read process starttime from /proc/PID/stat (field 22 after the closing paren)
	statContent := readFile(fmt.Sprintf("/proc/%s/stat", pid))
	if statContent == "" {
		return 0
	}
	// Find the closing paren of comm field, then we need field 22 after that
	parenIdx := strings.LastIndex(statContent, ")")
	if parenIdx < 0 {
		return 0
	}
	fields := strings.Fields(statContent[parenIdx+2:])
	if len(fields) < 20 {
		return 0
	}
	// starttime is the 20th field after the comm (since state=0, ppid=1, ...)
	starttime := fields[19]
	ticks, err := strconv.ParseInt(starttime, 10, 64)
	if err != nil {
		return 0
	}
	// Get system uptime
	uptimeContent := readFile("/proc/uptime")
	if uptimeContent == "" {
		return 0
	}
	uptimeParts := strings.Fields(uptimeContent)
	if len(uptimeParts) < 1 {
		return 0
	}
	systemUptime, err := strconv.ParseFloat(uptimeParts[0], 64)
	if err != nil {
		return 0
	}
	// Get clock ticks per second (usually 100)
	clkTck := int64(100)
	clkTckContent := readFile("/proc/stat")
	if clkTckContent != "" {
		for _, line := range strings.Split(clkTckContent, "\n") {
			if strings.HasPrefix(line, "btime ") {
				// Can use this to double-check but not needed
				break
			}
		}
	}
	processUptime := float64(ticks) / float64(clkTck)
	containerUptime := systemUptime - processUptime
	if containerUptime < 0 {
		containerUptime = 0
	}
	return containerUptime
}

func readFile(path string) string {
	data, err := os.ReadFile(path)
	if err != nil {
		return ""
	}
	return string(data)
}

func tailFile(path string, maxLines int) string {
	data, err := os.ReadFile(path)
	if err != nil {
		return ""
	}
	lines := strings.Split(strings.TrimSpace(string(data)), "\n")
	if len(lines) > maxLines {
		lines = lines[len(lines)-maxLines:]
	}
	return strings.Join(lines, "\n")
}

// GetContainerMemoryCgroupUsage returns memory.usage_in_bytes for a container, for usage-only queries.
func (m *Manager) GetContainerMemoryCgroupUsage(id int) int64 {
	c := config.FindContainer(id)
	if c == nil {
		return 0
	}
	return readIntCommand(fmt.Sprintf(
		"cat /sys/fs/cgroup/lxc/%[1]s/memory.current 2>/dev/null || "+
			"cat /sys/fs/cgroup/lxc.payload.%[1]s/memory.current 2>/dev/null || "+
			"cat /sys/fs/cgroup/memory/lxc/%[1]s/memory.usage_in_bytes 2>/dev/null || echo 0", shellQuote(c.LxcName())))
}

func shellQuote(s string) string {
	return fmt.Sprintf("'%s'", strings.ReplaceAll(s, "'", "'\\''"))
}

func readIntCommand(cmdStr string) int64 {
	cmd := exec.Command("sh", "-c", cmdStr)
	out, err := cmd.Output()
	if err != nil {
		return 0
	}
	val, _ := strconv.ParseInt(strings.TrimSpace(string(out)), 10, 64)
	return val
}

func generateRandomString(length int) string {
	b := make([]byte, length)
	rand.Read(b)
	return hex.EncodeToString(b)[:length]
}

// lastTrafficSnapshot stores previous network byte counts for delta calculation
var (
	lastTrafficSnapshot   = map[string]trafficSample{}
	lastTrafficSnapshotMu sync.Mutex
)

type trafficSample struct {
	RXBytes uint64
	TXBytes uint64
}

// AccumulateTraffic tracks container network traffic usage (delta-based, called periodically)
func (m *Manager) AccumulateTraffic() {
	currentMonth := time.Now().Format("2006-01")
	lastTrafficSnapshotMu.Lock()
	defer lastTrafficSnapshotMu.Unlock()

	changed := false
	for i := range config.AppConfig.Containers {
		c := &config.AppConfig.Containers[i]
		if c.Status != "running" {
			// Remove snapshot for stopped containers
			delete(lastTrafficSnapshot, c.LxcName())
			continue
		}
		if c.IsKVM() {
			continue
		}
		// Reset if new month
		if c.TrafficResetDate != currentMonth {
			c.TrafficUsedRX = 0
			c.TrafficUsedTX = 0
			c.TrafficResetDate = currentMonth
			delete(lastTrafficSnapshot, c.LxcName())
			changed = true
		}
		rx, tx := m.getContainerNetworkBytes(c.LxcName())
		prev, exists := lastTrafficSnapshot[c.LxcName()]
		// Only add the DELTA (increment since last snapshot)
		if exists && rx >= prev.RXBytes && tx >= prev.TXBytes {
			deltaRX := int64(rx - prev.RXBytes)
			deltaTX := int64(tx - prev.TXBytes)
			if deltaRX > 0 || deltaTX > 0 {
				c.TrafficUsedRX += deltaRX
				c.TrafficUsedTX += deltaTX
				changed = true
			}
		}
		lastTrafficSnapshot[c.LxcName()] = trafficSample{RXBytes: rx, TXBytes: tx}
	}
	if changed {
		config.SaveConfig()
	}
}

// GetTrafficInfo returns traffic usage info for a container
func (m *Manager) GetTrafficInfo(id int) map[string]interface{} {
	c := config.FindContainer(id)
	if c == nil {
		return nil
	}
	currentMonth := time.Now().Format("2006-01")
	if c.TrafficResetDate != currentMonth {
		c.TrafficUsedRX = 0
		c.TrafficUsedTX = 0
		c.TrafficResetDate = currentMonth
		config.SaveConfig()
	}

	// Accumulate new delta since last call
	lastTrafficSnapshotMu.Lock()
	if c.Status == "running" {
		rx, tx := m.getContainerNetworkBytes(c.LxcName())
		prev, exists := lastTrafficSnapshot[c.LxcName()]
		if exists && rx >= prev.RXBytes && tx >= prev.TXBytes {
			c.TrafficUsedRX += int64(rx - prev.RXBytes)
			c.TrafficUsedTX += int64(tx - prev.TXBytes)
			config.SaveConfig()
		}
		lastTrafficSnapshot[c.LxcName()] = trafficSample{RXBytes: rx, TXBytes: tx}
	}
	lastTrafficSnapshotMu.Unlock()

	totalUsed := c.TrafficUsedRX + c.TrafficUsedTX
	limitGB := 0
	usedPct := 0.0
	if c.TrafficMode == "in_out" {
		limitGB = c.TrafficInGB + c.TrafficOutGB
		inUsed := float64(c.TrafficUsedRX)
		outUsed := float64(c.TrafficUsedTX)
		inLimit := float64(c.TrafficInGB) * 1073741824
		outLimit := float64(c.TrafficOutGB) * 1073741824
		inPct := 0.0
		outPct := 0.0
		if c.TrafficInGB > 0 {
			inPct = inUsed / inLimit * 100
		}
		if c.TrafficOutGB > 0 {
			outPct = outUsed / outLimit * 100
		}
		usedPct = inPct
		if outPct > usedPct {
			usedPct = outPct
		}
	} else {
		limitGB = c.MonthlyTrafficGB
		if limitGB > 0 {
			usedPct = float64(totalUsed) / float64(limitGB*1073741824) * 100
		}
	}

	return map[string]interface{}{
		"total_used_bytes": totalUsed,
		"rx_used_bytes":    c.TrafficUsedRX,
		"tx_used_bytes":    c.TrafficUsedTX,
		"mode":             c.TrafficMode,
		"limit_gb":         limitGB,
		"in_limit_gb":      c.TrafficInGB,
		"out_limit_gb":     c.TrafficOutGB,
		"used_pct":         usedPct,
		"reset_date":       c.TrafficResetDate,
	}
}

// ToJSON converts data to JSON bytes
func ToJSON(v interface{}) ([]byte, error) {
	return json.MarshalIndent(v, "", "  ")
}
