package api

import (
	"context"
	"crypto/rand"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"net/http"
	"os"
	"os/exec"
	"path/filepath"
	"regexp"
	"runtime"
	"strings"
	"sync"
	"time"

	"clicd/internal/config"
	"clicd/internal/kvm"
	"clicd/internal/lxc"
	"clicd/internal/safehttp"
)

// ImageInfo represents a template image with its download/enable status.
type ImageInfo struct {
	ID              string `json:"id"`
	Name            string `json:"name"`
	Type            string `json:"type"`
	Distro          string `json:"distro"`
	Release         string `json:"release"`
	Arch            string `json:"arch"`
	Description     string `json:"description"`
	Downloaded      bool   `json:"downloaded"`
	Enabled         bool   `json:"enabled"`
	Downloading     bool   `json:"downloading"`
	Progress        int    `json:"progress"`
	DownloadedBytes int64  `json:"downloaded_bytes"`
	TotalBytes      int64  `json:"total_bytes"`
	Stage           string `json:"stage,omitempty"`
	Error           string `json:"error,omitempty"`
	SizeBytes       int64  `json:"size_bytes"`
	ManualPath      string `json:"manual_path,omitempty"`
	Desktop         string `json:"desktop,omitempty"`
	Provisioner     string `json:"provisioner,omitempty"`
	Custom          bool   `json:"custom,omitempty"`
	SHA256          string `json:"sha256,omitempty"`
}

var customImageFieldPattern = regexp.MustCompile(`^[a-z0-9][a-z0-9._-]{0,63}$`)
var sha256Pattern = regexp.MustCompile(`^[a-fA-F0-9]{64}$`)

var imageDownloadsMu sync.Mutex
var imageDownloads = map[string]*imageDownloadStatus{}
var lxcImageCacheMu sync.Mutex
var lxcImageDownloadMu sync.Mutex
var lxcImageDownloadActive bool

type imageDownloadStatus struct {
	Downloading     bool
	Progress        int
	DownloadedBytes int64
	TotalBytes      int64
	Stage           string
	Error           string
	Cancel          context.CancelFunc
	UpdatedAt       time.Time
}

type imageDownloadSnapshot struct {
	Downloading     bool
	Progress        int
	DownloadedBytes int64
	TotalBytes      int64
	Stage           string
	Error           string
}

func imageDownloadInfo(id string) imageDownloadSnapshot {
	imageDownloadsMu.Lock()
	defer imageDownloadsMu.Unlock()
	st := imageDownloads[id]
	if st == nil {
		return imageDownloadSnapshot{}
	}
	return imageDownloadSnapshot{
		Downloading:     st.Downloading,
		Progress:        st.Progress,
		DownloadedBytes: st.DownloadedBytes,
		TotalBytes:      st.TotalBytes,
		Stage:           st.Stage,
		Error:           st.Error,
	}
}

func startImageDownload(id, stage string) (context.Context, bool) {
	imageDownloadsMu.Lock()
	defer imageDownloadsMu.Unlock()
	if st := imageDownloads[id]; st != nil && st.Downloading {
		return nil, false
	}
	ctx, cancel := context.WithCancel(context.Background())
	imageDownloads[id] = &imageDownloadStatus{
		Downloading: true,
		Stage:       stage,
		Cancel:      cancel,
		UpdatedAt:   time.Now(),
	}
	return ctx, true
}

func updateImageDownload(id string, update func(*imageDownloadStatus)) {
	imageDownloadsMu.Lock()
	defer imageDownloadsMu.Unlock()
	st := imageDownloads[id]
	if st == nil {
		return
	}
	update(st)
	st.UpdatedAt = time.Now()
}

func finishImageDownload(id string, err error) {
	imageDownloadsMu.Lock()
	defer imageDownloadsMu.Unlock()
	st := imageDownloads[id]
	if st == nil {
		return
	}
	st.Downloading = false
	st.Cancel = nil
	st.UpdatedAt = time.Now()
	if err != nil {
		st.Error = err.Error()
		return
	}
	delete(imageDownloads, id)
}

func clearImageDownload(id string) {
	imageDownloadsMu.Lock()
	delete(imageDownloads, id)
	imageDownloadsMu.Unlock()
}

func isImageDownloadActive(id string) bool {
	imageDownloadsMu.Lock()
	defer imageDownloadsMu.Unlock()
	st := imageDownloads[id]
	return st != nil && st.Downloading
}

func beginLXCImageDownload() bool {
	lxcImageDownloadMu.Lock()
	defer lxcImageDownloadMu.Unlock()
	if lxcImageDownloadActive {
		return false
	}
	lxcImageDownloadActive = true
	return true
}

func endLXCImageDownload() {
	lxcImageDownloadMu.Lock()
	lxcImageDownloadActive = false
	lxcImageDownloadMu.Unlock()
}

func lxcImageDownloadTempName(id string) string {
	return fmt.Sprintf("clicd-img-dl-%s", id)
}

func cleanupLXCImageDownloadTemp(id string) {
	tmpName := lxcImageDownloadTempName(id)
	exec.Command("lxc-destroy", "-n", tmpName, "-f").Run()
	os.RemoveAll(filepath.Join("/var/lib/lxc", tmpName))
}

func cleanupOldImageDownloadErrors() {
	imageDownloadsMu.Lock()
	defer imageDownloadsMu.Unlock()
	cutoff := time.Now().Add(-10 * time.Minute)
	for id, st := range imageDownloads {
		if !st.Downloading && st.UpdatedAt.Before(cutoff) {
			delete(imageDownloads, id)
		}
	}
}

// imageDownloadedInfo returns whether the image is downloaded and its total size in bytes.
func imageDownloadedInfo(templateID string) (bool, int64) {
	cachePath, ok := officialLXCImageCachePath(templateID)
	if !ok {
		return false, 0
	}
	info, err := os.Stat(cachePath)
	if err != nil || !info.IsDir() {
		return false, 0
	}
	for _, candidate := range []string{
		filepath.Join(cachePath, "rootfs.tar.xz"),
		filepath.Join(cachePath, "meta.tar.xz"),
		filepath.Join(cachePath, "default", "rootfs.tar.xz"),
		filepath.Join(cachePath, "default", "meta.tar.xz"),
	} {
		if fileInfo, err := os.Stat(candidate); err == nil && !fileInfo.IsDir() {
			return true, fileInfo.Size()
		}
	}
	return false, 0
}

func officialLXCImageCachePath(templateID string) (string, bool) {
	arch := "amd64"
	if runtime.GOARCH == "arm64" {
		arch = "arm64"
	}
	base := "/var/cache/lxc/download"
	switch templateID {
	case "ubuntu-noble":
		return filepath.Join(base, "ubuntu", "noble", arch), true
	case "ubuntu-jammy":
		return filepath.Join(base, "ubuntu", "jammy", arch), true
	case "debian-trixie":
		return filepath.Join(base, "debian", "trixie", arch), true
	case "debian-bookworm":
		return filepath.Join(base, "debian", "bookworm", arch), true
	case "debian-bullseye":
		return filepath.Join(base, "debian", "bullseye", arch), true
	case "alpine-3.21":
		return filepath.Join(base, "alpine", "3.21", arch), true
	case "centos-9-stream":
		return filepath.Join(base, "centos", "9-Stream", arch), true
	case "archlinux-current":
		return filepath.Join(base, "archlinux", "current", arch), true
	case "fedora-44":
		return filepath.Join(base, "fedora", "44", arch), true
	case "rockylinux-10":
		return filepath.Join(base, "rockylinux", "10", arch), true
	default:
		return "", false
	}
}

func lxcTemplateDownloadedInfo(template lxc.Template) (bool, int64) {
	if template.Custom {
		return lxc.CustomImageDownloadedInfo(template.ID)
	}
	return imageDownloadedInfo(template.ID)
}

// getEnabledImageSet returns the set of enabled image IDs.
// If none have been explicitly set, all templates are enabled by default.
func getEnabledImageSet() map[string]bool {
	set := make(map[string]bool)
	if len(config.AppConfig.EnabledImages) == 0 {
		for _, t := range lxc.GetTemplates() {
			set[t.ID] = true
		}
		for _, t := range kvm.GetImages() {
			set[t.ID] = true
		}
	} else {
		for _, id := range config.AppConfig.EnabledImages {
			set[id] = true
		}
	}
	return set
}

// HandleImages returns the list of templates with download/enable status.
func HandleImages(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodGet {
		jsonResponse(w, http.StatusMethodNotAllowed, APIResponse{Success: false, Message: "Method not allowed"})
		return
	}
	if !requireScope(w, r, "image:read") {
		return
	}

	enabledSet := getEnabledImageSet()
	cleanupOldImageDownloadErrors()
	kvmAvailable := hostKVMAvailable()

	templates := lxc.GetTemplates()
	kvmImages := []kvm.Image{}
	if kvmAvailable {
		kvmImages = kvm.GetImages()
	}
	images := make([]ImageInfo, 0, len(templates))
	for _, t := range templates {
		dl := imageDownloadInfo(t.ID)
		downloaded, size := lxcTemplateDownloadedInfo(t)
		images = append(images, ImageInfo{
			ID:              t.ID,
			Name:            t.Name,
			Type:            config.VirtualizationLXC,
			Distro:          t.Distro,
			Release:         t.Release,
			Arch:            t.Arch,
			Description:     t.Description,
			Downloaded:      downloaded,
			Enabled:         enabledSet[t.ID],
			Downloading:     dl.Downloading,
			Progress:        dl.Progress,
			DownloadedBytes: dl.DownloadedBytes,
			TotalBytes:      dl.TotalBytes,
			Stage:           dl.Stage,
			Error:           dl.Error,
			SizeBytes:       size,
			Custom:          t.Custom,
			SHA256:          t.SHA256,
		})
	}
	for _, t := range kvmImages {
		dl := imageDownloadInfo(t.ID)
		downloaded, size := kvm.ImageDownloadedInfo(t.ID)
		manualPath := ""
		if t.IsWindows() {
			manualPath = kvm.ImagePath(t.ID)
		}
		images = append(images, ImageInfo{
			ID:              t.ID,
			Name:            t.Name,
			Type:            config.VirtualizationKVM,
			Distro:          t.Distro,
			Release:         t.Release,
			Arch:            t.Arch,
			Description:     t.Description,
			Downloaded:      downloaded,
			Enabled:         enabledSet[t.ID],
			Downloading:     dl.Downloading,
			Progress:        dl.Progress,
			DownloadedBytes: dl.DownloadedBytes,
			TotalBytes:      dl.TotalBytes,
			Stage:           dl.Stage,
			Error:           dl.Error,
			SizeBytes:       size,
			ManualPath:      manualPath,
			Desktop:         t.Desktop,
			Provisioner:     t.Provisioner,
			Custom:          t.Custom,
			SHA256:          t.SHA256,
		})
	}

	jsonResponse(w, http.StatusOK, APIResponse{Success: true, Data: images})
}

// HandleCustomKVMImages creates or removes administrator-defined LXC/KVM image sources.
func HandleCustomKVMImages(w http.ResponseWriter, r *http.Request) {
	switch r.Method {
	case http.MethodPost:
		if !requireScope(w, r, "image:download") {
			return
		}
		handleCustomKVMImageCreate(w, r)
	case http.MethodDelete:
		if !requireScope(w, r, "image:delete") {
			return
		}
		handleCustomKVMImageDelete(w, r)
	default:
		jsonResponse(w, http.StatusMethodNotAllowed, APIResponse{Success: false, Message: "Method not allowed"})
	}
}

func handleCustomKVMImageCreate(w http.ResponseWriter, r *http.Request) {
	var req struct {
		Type        string `json:"type"`
		Name        string `json:"name"`
		Description string `json:"description"`
		Distro      string `json:"distro"`
		Release     string `json:"release"`
		Arch        string `json:"arch"`
		URL         string `json:"url"`
		Provisioner string `json:"provisioner"`
		SHA256      string `json:"sha256"`
	}
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		jsonResponse(w, http.StatusBadRequest, APIResponse{Success: false, Message: "Invalid request body"})
		return
	}
	req.Name = strings.TrimSpace(req.Name)
	req.Type = strings.ToLower(strings.TrimSpace(req.Type))
	if req.Type == "" {
		req.Type = config.VirtualizationKVM
	}
	req.Description = strings.TrimSpace(req.Description)
	req.Distro = strings.ToLower(strings.TrimSpace(req.Distro))
	req.Release = strings.ToLower(strings.TrimSpace(req.Release))
	req.Arch = strings.ToLower(strings.TrimSpace(req.Arch))
	req.URL = strings.TrimSpace(req.URL)
	req.Provisioner = strings.ToLower(strings.TrimSpace(req.Provisioner))
	req.SHA256 = strings.ToLower(strings.TrimSpace(req.SHA256))

	if req.Name == "" || len(req.Name) > 100 {
		jsonResponse(w, http.StatusBadRequest, APIResponse{Success: false, Message: "name must be between 1 and 100 characters"})
		return
	}
	if len(req.Description) > 500 {
		jsonResponse(w, http.StatusBadRequest, APIResponse{Success: false, Message: "description must not exceed 500 characters"})
		return
	}
	if req.Arch != runtime.GOARCH || (req.Arch != "amd64" && req.Arch != "arm64") {
		jsonResponse(w, http.StatusBadRequest, APIResponse{Success: false, Message: "image architecture must match the host architecture"})
		return
	}
	if req.Type == config.VirtualizationLXC {
		if !customImageFieldPattern.MatchString(req.Distro) {
			jsonResponse(w, http.StatusBadRequest, APIResponse{Success: false, Message: "distro contains unsupported characters"})
			return
		}
		if !customImageFieldPattern.MatchString(req.Release) {
			jsonResponse(w, http.StatusBadRequest, APIResponse{Success: false, Message: "release contains unsupported characters"})
			return
		}
	} else if req.Type == config.VirtualizationKVM {
		switch req.Provisioner {
		case config.KVMProvisionerLinuxCloudInit:
			if !customImageFieldPattern.MatchString(req.Distro) {
				jsonResponse(w, http.StatusBadRequest, APIResponse{Success: false, Message: "distro contains unsupported characters"})
				return
			}
			if !customImageFieldPattern.MatchString(req.Release) {
				jsonResponse(w, http.StatusBadRequest, APIResponse{Success: false, Message: "release contains unsupported characters"})
				return
			}
		case config.KVMProvisionerWindows10:
			if req.Arch != "amd64" {
				jsonResponse(w, http.StatusBadRequest, APIResponse{Success: false, Message: "Windows unattended installation currently requires an amd64 host"})
				return
			}
			req.Distro = "windows"
			req.Release = "10"
		case config.KVMProvisionerWindows11:
			if req.Arch != "amd64" {
				jsonResponse(w, http.StatusBadRequest, APIResponse{Success: false, Message: "Windows unattended installation currently requires an amd64 host"})
				return
			}
			req.Distro = "windows"
			req.Release = "11"
		default:
			jsonResponse(w, http.StatusBadRequest, APIResponse{Success: false, Message: "unsupported unattended installation template"})
			return
		}
	} else {
		jsonResponse(w, http.StatusBadRequest, APIResponse{Success: false, Message: "type must be lxc or kvm"})
		return
	}
	if len(req.URL) > 4096 {
		jsonResponse(w, http.StatusBadRequest, APIResponse{Success: false, Message: "url must not exceed 4096 characters"})
		return
	}
	if _, err := safehttp.ValidateURL(req.URL); err != nil {
		jsonResponse(w, http.StatusBadRequest, APIResponse{Success: false, Message: err.Error()})
		return
	}
	if req.SHA256 != "" && !sha256Pattern.MatchString(req.SHA256) {
		jsonResponse(w, http.StatusBadRequest, APIResponse{Success: false, Message: "sha256 must contain exactly 64 hexadecimal characters"})
		return
	}
	if req.Type == config.VirtualizationLXC {
		for _, existing := range lxc.GetTemplates() {
			if strings.EqualFold(existing.Name, req.Name) {
				jsonResponse(w, http.StatusConflict, APIResponse{Success: false, Message: "an image with this name already exists"})
				return
			}
			if existing.Custom && existing.URL == req.URL {
				jsonResponse(w, http.StatusConflict, APIResponse{Success: false, Message: "this image URL is already registered"})
				return
			}
		}
	} else {
		for _, existing := range kvm.GetImages() {
			if strings.EqualFold(existing.Name, req.Name) {
				jsonResponse(w, http.StatusConflict, APIResponse{Success: false, Message: "an image with this name already exists"})
				return
			}
			if existing.Custom && existing.URL == req.URL {
				jsonResponse(w, http.StatusConflict, APIResponse{Success: false, Message: "this image URL is already registered"})
				return
			}
		}
	}

	random := make([]byte, 5)
	if _, err := rand.Read(random); err != nil {
		jsonResponse(w, http.StatusInternalServerError, APIResponse{Success: false, Message: "failed to generate image ID"})
		return
	}
	createdAt := time.Now().Format("2006-01-02 15:04:05")
	if req.Type == config.VirtualizationLXC {
		image := config.CustomLXCImage{
			ID:          "custom-lxc-" + hex.EncodeToString(random),
			Name:        req.Name,
			Description: req.Description,
			Distro:      req.Distro,
			Release:     req.Release,
			Arch:        req.Arch,
			URL:         req.URL,
			SHA256:      req.SHA256,
			CreatedAt:   createdAt,
		}
		if err := config.AddCustomLXCImage(image); err != nil {
			jsonResponse(w, http.StatusInternalServerError, APIResponse{Success: false, Message: "failed to save custom image: " + err.Error()})
			return
		}
		jsonResponse(w, http.StatusCreated, APIResponse{Success: true, Message: "Custom image added", Data: image})
		return
	}
	image := config.CustomKVMImage{
		ID: "custom-kvm-" + hex.EncodeToString(random), Name: req.Name, Description: req.Description,
		Distro: req.Distro, Release: req.Release, Arch: req.Arch, URL: req.URL,
		Provisioner: req.Provisioner, SHA256: req.SHA256, CreatedAt: createdAt,
	}
	if err := config.AddCustomKVMImage(image); err != nil {
		jsonResponse(w, http.StatusInternalServerError, APIResponse{Success: false, Message: "failed to save custom image: " + err.Error()})
		return
	}
	jsonResponse(w, http.StatusCreated, APIResponse{Success: true, Message: "Custom image added", Data: image})
}

func handleCustomKVMImageDelete(w http.ResponseWriter, r *http.Request) {
	var req struct {
		ID string `json:"id"`
	}
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil || strings.TrimSpace(req.ID) == "" {
		jsonResponse(w, http.StatusBadRequest, APIResponse{Success: false, Message: "id required"})
		return
	}
	req.ID = strings.TrimSpace(req.ID)
	kvmImage := kvm.FindImage(req.ID)
	lxcImage := lxc.FindTemplate(req.ID)
	isCustomKVM := kvmImage != nil && kvmImage.Custom
	isCustomLXC := lxcImage != nil && lxcImage.Custom
	if !isCustomKVM && !isCustomLXC {
		jsonResponse(w, http.StatusNotFound, APIResponse{Success: false, Message: "Custom image not found"})
		return
	}
	if isImageDownloadActive(req.ID) {
		jsonResponse(w, http.StatusConflict, APIResponse{Success: false, Message: "Image is downloading; cancel it before removing the source"})
		return
	}
	for i := range config.AppConfig.Containers {
		if config.AppConfig.Containers[i].Template == req.ID {
			jsonResponse(w, http.StatusConflict, APIResponse{Success: false, Message: "This image is still used by a container"})
			return
		}
	}
	for i := range config.AppConfig.Tasks {
		task := &config.AppConfig.Tasks[i]
		if task.Status != "pending" && task.Status != "running" {
			continue
		}
		var taskConfig struct {
			TemplateID string `json:"template_id"`
		}
		_ = json.Unmarshal([]byte(task.Config), &taskConfig)
		if task.TemplateID == req.ID || taskConfig.TemplateID == req.ID {
			jsonResponse(w, http.StatusConflict, APIResponse{Success: false, Message: "This image is still referenced by an active task"})
			return
		}
	}
	var deleteErr error
	if isCustomLXC {
		deleteErr = lxc.DeleteCustomImage(req.ID)
	} else {
		deleteErr = kvm.DeleteImage(req.ID)
	}
	if deleteErr != nil {
		jsonResponse(w, http.StatusInternalServerError, APIResponse{Success: false, Message: "Failed to delete image cache: " + deleteErr.Error()})
		return
	}
	removeImageEnabled(req.ID)
	var removed bool
	var err error
	if isCustomLXC {
		removed, err = config.RemoveCustomLXCImage(req.ID)
	} else {
		removed, err = config.RemoveCustomKVMImage(req.ID)
	}
	if err != nil {
		jsonResponse(w, http.StatusInternalServerError, APIResponse{Success: false, Message: "Failed to remove custom image: " + err.Error()})
		return
	}
	if !removed {
		jsonResponse(w, http.StatusNotFound, APIResponse{Success: false, Message: "Custom image not found"})
		return
	}
	clearImageDownload(req.ID)
	jsonResponse(w, http.StatusOK, APIResponse{Success: true, Message: "Custom image removed"})
}

// HandleImageDownload starts a template image download in the background.
func HandleImageDownload(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		jsonResponse(w, http.StatusMethodNotAllowed, APIResponse{Success: false, Message: "Method not allowed"})
		return
	}
	if !requireScope(w, r, "image:download") {
		return
	}

	var req struct {
		TemplateID string `json:"template_id"`
	}
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.TemplateID == "" {
		jsonResponse(w, http.StatusBadRequest, APIResponse{Success: false, Message: "template_id required"})
		return
	}
	tmpl := lxc.FindTemplate(req.TemplateID)
	if tmpl == nil {
		image := kvm.FindImage(req.TemplateID)
		if image == nil {
			jsonResponse(w, http.StatusNotFound, APIResponse{Success: false, Message: "Template not found"})
			return
		}
		if !hostKVMAvailable() {
			jsonResponse(w, http.StatusForbidden, APIResponse{Success: false, Message: "KVM is not available on this host"})
			return
		}
		if _, err := config.SelectStoragePoolForContent(config.StorageContentImages, "", 1024*1024*1024); err != nil {
			jsonResponse(w, http.StatusConflict, APIResponse{Success: false, Message: err.Error()})
			return
		}
		if ok, _ := kvm.ImageDownloadedInfo(image.ID); ok {
			ensureImageEnabled(image.ID)
			clearImageDownload(image.ID)
			jsonResponse(w, http.StatusOK, APIResponse{Success: true, Message: "Already downloaded"})
			return
		}
		ctx, ok := startImageDownload(image.ID, "downloading")
		if !ok {
			jsonResponse(w, http.StatusConflict, APIResponse{Success: false, Message: "Already downloading"})
			return
		}
		go func(image kvm.Image) {
			err := kvm.DownloadImageWithProgress(ctx, image, func(p kvm.DownloadProgress) {
				updateImageDownload(image.ID, func(st *imageDownloadStatus) {
					if p.Stage != "" {
						st.Stage = p.Stage
					}
					if p.DownloadedBytes > 0 || p.TotalBytes > 0 {
						st.DownloadedBytes = p.DownloadedBytes
						st.TotalBytes = p.TotalBytes
					}
					st.Progress = p.Percent
				})
			})
			if err != nil {
				if ctx.Err() != nil {
					os.Remove(kvm.ImagePath(image.ID) + ".tmp")
					os.Remove(kvm.ImagePath(image.ID))
					finishImageDownload(image.ID, nil)
					return
				}
				finishImageDownload(image.ID, err)
				return
			}
			ensureImageEnabled(image.ID)
			finishImageDownload(image.ID, nil)
		}(*image)
		jsonResponse(w, http.StatusAccepted, APIResponse{Success: true, Message: "Download started"})
		return
	}
	if !beginLXCImageDownload() {
		jsonResponse(w, http.StatusConflict, APIResponse{Success: false, Message: "Another LXC image download is active"})
		return
	}
	lxcDownloadHandedOff := false
	defer func() {
		if !lxcDownloadHandedOff {
			endLXCImageDownload()
		}
	}()
	imagePool, err := config.SelectStoragePoolForContent(
		config.StorageContentImages,
		"",
		dirSizeBytes("/var/cache/lxc/download")+1024*1024*1024,
	)
	if err != nil {
		jsonResponse(w, http.StatusConflict, APIResponse{Success: false, Message: err.Error()})
		return
	}
	if err := ensureLXCImageCachePool(*imagePool); err != nil {
		jsonResponse(w, http.StatusInternalServerError, APIResponse{Success: false, Message: err.Error()})
		return
	}

	// Already downloaded? Just enable if needed.
	if downloaded, _ := lxcTemplateDownloadedInfo(*tmpl); downloaded {
		ensureImageEnabled(tmpl.ID)
		clearImageDownload(tmpl.ID)
		jsonResponse(w, http.StatusOK, APIResponse{Success: true, Message: "Already downloaded"})
		return
	}

	startStage := "lxc-create"
	if tmpl.Custom {
		startStage = "downloading"
	}
	ctx, ok := startImageDownload(tmpl.ID, startStage)
	if !ok {
		jsonResponse(w, http.StatusConflict, APIResponse{Success: false, Message: "Already downloading"})
		return
	}

	if tmpl.Custom {
		go func(tmpl lxc.Template) {
			defer endLXCImageDownload()
			err := lxc.DownloadCustomImageWithProgress(ctx, tmpl, func(progress lxc.CustomImageDownloadProgress) {
				updateImageDownload(tmpl.ID, func(status *imageDownloadStatus) {
					status.Stage = progress.Stage
					status.DownloadedBytes = progress.DownloadedBytes
					status.TotalBytes = progress.TotalBytes
					status.Progress = progress.Percent
				})
			})
			if err != nil {
				if ctx.Err() != nil {
					_ = os.Remove(lxc.CustomImagePath(tmpl.ID) + ".tmp")
					_ = os.Remove(lxc.CustomImagePath(tmpl.ID))
					finishImageDownload(tmpl.ID, nil)
					return
				}
				finishImageDownload(tmpl.ID, err)
				return
			}
			ensureImageEnabled(tmpl.ID)
			finishImageDownload(tmpl.ID, nil)
		}(*tmpl)
		lxcDownloadHandedOff = true
		jsonResponse(w, http.StatusAccepted, APIResponse{Success: true, Message: "Download started"})
		return
	}

	go func(tmpl lxc.Template) {
		defer endLXCImageDownload()
		// Download via lxc-create with a temp container, then destroy it.
		tmpName := lxcImageDownloadTempName(tmpl.ID)
		args := []string{"-n", tmpName, "-t", "download", "--",
			"-d", tmpl.Distro, "-r", tmpl.Release, "-a", tmpl.Arch}
		if tmpl.Variant != "" {
			args = append(args, "--variant", tmpl.Variant)
		}
		updateImageDownload(tmpl.ID, func(st *imageDownloadStatus) {
			st.Stage = "lxc-create"
		})
		cmd := exec.CommandContext(ctx, "lxc-create", args...)
		output, err := runLXCImageDownloadCommand(cmd, tmpl.ID)

		// Clean up the temp container unconditionally.
		cleanupLXCImageDownloadTemp(tmpl.ID)

		if err != nil {
			if ctx.Err() != nil {
				finishImageDownload(tmpl.ID, nil)
				return
			}
			err = fmt.Errorf("Download failed: %v, output: %s", err, string(output))
			finishImageDownload(tmpl.ID, err)
			return
		}
		ensureImageEnabled(tmpl.ID)
		finishImageDownload(tmpl.ID, nil)
	}(*tmpl)
	lxcDownloadHandedOff = true

	jsonResponse(w, http.StatusAccepted, APIResponse{Success: true, Message: "Download started"})
}

type lxcImageDownloadCommandResult struct {
	output []byte
	err    error
}

func runLXCImageDownloadCommand(cmd *exec.Cmd, templateID string) ([]byte, error) {
	startedAt := time.Now()
	done := make(chan lxcImageDownloadCommandResult, 1)
	go func() {
		output, err := cmd.CombinedOutput()
		done <- lxcImageDownloadCommandResult{output: output, err: err}
	}()

	ticker := time.NewTicker(time.Second)
	defer ticker.Stop()
	var lastBytes int64
	for {
		select {
		case result := <-done:
			return result.output, result.err
		case <-ticker.C:
			downloadedBytes := newestLXCRootfsDownloadSize(startedAt)
			if downloadedBytes <= 0 || downloadedBytes == lastBytes {
				continue
			}
			lastBytes = downloadedBytes
			updateImageDownload(templateID, func(st *imageDownloadStatus) {
				st.Stage = "downloading"
				st.DownloadedBytes = downloadedBytes
			})
		}
	}
}

func newestLXCRootfsDownloadSize(startedAt time.Time) int64 {
	matches, _ := filepath.Glob("/tmp/tmp.*/rootfs.tar.xz")
	var newestTime time.Time
	var newestSize int64
	for _, match := range matches {
		info, err := os.Stat(match)
		if err != nil || info.IsDir() || info.ModTime().Before(startedAt.Add(-5*time.Second)) {
			continue
		}
		if info.ModTime().After(newestTime) {
			newestTime = info.ModTime()
			newestSize = info.Size()
		}
	}
	return newestSize
}

func ensureLXCImageCachePool(pool config.StoragePool) error {
	lxcImageCacheMu.Lock()
	defer lxcImageCacheMu.Unlock()

	cachePath := "/var/cache/lxc/download"
	targetPath := filepath.Join(pool.Path, "images", "lxc")
	targetAbs, err := filepath.Abs(targetPath)
	if err != nil {
		return err
	}
	if err := os.MkdirAll(targetAbs, 0755); err != nil {
		return fmt.Errorf("failed to create LXC image storage: %v", err)
	}

	info, err := os.Lstat(cachePath)
	if os.IsNotExist(err) {
		if err := os.MkdirAll(filepath.Dir(cachePath), 0755); err != nil {
			return err
		}
		return os.Symlink(targetAbs, cachePath)
	}
	if err != nil {
		return err
	}

	sourcePath := cachePath
	linked := info.Mode()&os.ModeSymlink != 0
	if linked {
		sourcePath, err = filepath.EvalSymlinks(cachePath)
		if err != nil {
			return fmt.Errorf("failed to resolve LXC image cache: %v", err)
		}
	}
	sourceAbs, err := filepath.Abs(sourcePath)
	if err != nil {
		return err
	}
	if sourceAbs == targetAbs {
		return nil
	}
	if strings.HasPrefix(targetAbs, sourceAbs+string(os.PathSeparator)) || strings.HasPrefix(sourceAbs, targetAbs+string(os.PathSeparator)) {
		return fmt.Errorf("LXC image cache source and target must not be nested")
	}
	if !info.IsDir() && !linked {
		return fmt.Errorf("LXC image cache is not a directory: %s", cachePath)
	}

	if output, err := exec.Command("cp", "-a", sourceAbs+string(os.PathSeparator)+".", targetAbs+string(os.PathSeparator)).CombinedOutput(); err != nil {
		return fmt.Errorf("failed to migrate LXC image cache: %v, output: %s", err, strings.TrimSpace(string(output)))
	}

	tempLink := fmt.Sprintf("%s.clicd-new-%d", cachePath, time.Now().UnixNano())
	if err := os.Symlink(targetAbs, tempLink); err != nil {
		return err
	}
	if linked {
		if err := os.Rename(tempLink, cachePath); err != nil {
			_ = os.Remove(tempLink)
			return fmt.Errorf("failed to switch LXC image cache: %v", err)
		}
		if isManagedLXCImageCachePath(sourceAbs) {
			_ = os.RemoveAll(sourceAbs)
		}
		return nil
	}

	backupPath := fmt.Sprintf("%s.clicd-backup-%d", cachePath, time.Now().UnixNano())
	if err := os.Rename(cachePath, backupPath); err != nil {
		_ = os.Remove(tempLink)
		return fmt.Errorf("failed to prepare LXC image cache migration: %v", err)
	}
	if err := os.Rename(tempLink, cachePath); err != nil {
		_ = os.Rename(backupPath, cachePath)
		_ = os.Remove(tempLink)
		return fmt.Errorf("failed to activate LXC image storage: %v", err)
	}
	if err := os.RemoveAll(backupPath); err != nil {
		return fmt.Errorf("LXC image cache migrated but old cache cleanup failed: %v", err)
	}
	return nil
}

func isManagedLXCImageCachePath(path string) bool {
	path = filepath.Clean(path)
	for _, pool := range config.StoragePoolsForContent(config.StorageContentImages) {
		if path == filepath.Clean(filepath.Join(pool.Path, "images", "lxc")) {
			return true
		}
	}
	return path == filepath.Clean("/var/lib/clicd/images/lxc")
}

// HandleImageCancel cancels an in-progress image download.
func HandleImageCancel(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		jsonResponse(w, http.StatusMethodNotAllowed, APIResponse{Success: false, Message: "Method not allowed"})
		return
	}
	if !requireScope(w, r, "image:download") {
		return
	}
	var req struct {
		TemplateID string `json:"template_id"`
	}
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.TemplateID == "" {
		jsonResponse(w, http.StatusBadRequest, APIResponse{Success: false, Message: "template_id required"})
		return
	}

	imageDownloadsMu.Lock()
	st := imageDownloads[req.TemplateID]
	if st == nil || !st.Downloading || st.Cancel == nil {
		imageDownloadsMu.Unlock()
		jsonResponse(w, http.StatusNotFound, APIResponse{Success: false, Message: "No active download"})
		return
	}
	cancel := st.Cancel
	st.Stage = "canceling"
	st.UpdatedAt = time.Now()
	imageDownloadsMu.Unlock()

	cancel()
	if image := kvm.FindImage(req.TemplateID); image != nil {
		os.Remove(kvm.ImagePath(image.ID) + ".tmp")
		os.Remove(kvm.ImagePath(image.ID))
	}
	if tmpl := lxc.FindTemplate(req.TemplateID); tmpl != nil {
		if tmpl.Custom {
			_ = os.Remove(lxc.CustomImagePath(tmpl.ID) + ".tmp")
			_ = os.Remove(lxc.CustomImagePath(tmpl.ID))
		} else {
			go cleanupLXCImageDownloadTemp(tmpl.ID)
		}
	}
	jsonResponse(w, http.StatusOK, APIResponse{Success: true, Message: "Cancel requested"})
}

// HandleImageDelete deletes a cached template image from disk.
func HandleImageDelete(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodDelete {
		jsonResponse(w, http.StatusMethodNotAllowed, APIResponse{Success: false, Message: "Method not allowed"})
		return
	}
	if !requireScope(w, r, "image:delete") {
		return
	}

	var req struct {
		TemplateID string `json:"template_id"`
	}
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.TemplateID == "" {
		jsonResponse(w, http.StatusBadRequest, APIResponse{Success: false, Message: "template_id required"})
		return
	}
	if isImageDownloadActive(req.TemplateID) {
		jsonResponse(w, http.StatusConflict, APIResponse{Success: false, Message: "Image is downloading; cancel it before deleting"})
		return
	}

	tmpl := lxc.FindTemplate(req.TemplateID)
	if tmpl == nil {
		if image := kvm.FindImage(req.TemplateID); image != nil {
			if err := kvm.DeleteImage(image.ID); err != nil {
				jsonResponse(w, http.StatusInternalServerError, APIResponse{Success: false, Message: "Failed to delete image cache: " + err.Error()})
				return
			}
			removeImageEnabled(image.ID)
			jsonResponse(w, http.StatusOK, APIResponse{Success: true, Message: "Deleted"})
			return
		}
		jsonResponse(w, http.StatusNotFound, APIResponse{Success: false, Message: "Template not found"})
		return
	}
	if tmpl.Custom {
		if err := lxc.DeleteCustomImage(tmpl.ID); err != nil {
			jsonResponse(w, http.StatusInternalServerError, APIResponse{Success: false, Message: "Failed to delete image cache: " + err.Error()})
			return
		}
		removeImageEnabled(tmpl.ID)
		jsonResponse(w, http.StatusOK, APIResponse{Success: true, Message: "Deleted"})
		return
	}

	// Remove cache directory
	cachePath, ok := officialLXCImageCachePath(tmpl.ID)
	if !ok {
		jsonResponse(w, http.StatusBadRequest, APIResponse{Success: false, Message: "Template cache path is not managed by CLICD"})
		return
	}
	if err := os.RemoveAll(cachePath); err != nil {
		jsonResponse(w, http.StatusInternalServerError, APIResponse{
			Success: false,
			Message: fmt.Sprintf("Failed to delete image cache: %v", err),
		})
		return
	}

	// Remove from enabled list
	removeImageEnabled(tmpl.ID)

	jsonResponse(w, http.StatusOK, APIResponse{Success: true, Message: "Deleted"})
}

// HandleImageToggle enables or disables a template image.
func HandleImageToggle(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPut {
		jsonResponse(w, http.StatusMethodNotAllowed, APIResponse{Success: false, Message: "Method not allowed"})
		return
	}
	if !requireScope(w, r, "image:toggle") {
		return
	}

	var req struct {
		TemplateID string `json:"template_id"`
		Enabled    bool   `json:"enabled"`
	}
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.TemplateID == "" {
		jsonResponse(w, http.StatusBadRequest, APIResponse{Success: false, Message: "template_id required"})
		return
	}

	if req.Enabled {
		ensureImageEnabled(req.TemplateID)
	} else {
		removeImageEnabled(req.TemplateID)
	}

	jsonResponse(w, http.StatusOK, APIResponse{Success: true, Message: "OK"})
}

// HandleEnabledImages returns only the enabled AND downloaded templates.
// Used by container create / reinstall to filter available templates.
func HandleEnabledImages(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodGet {
		jsonResponse(w, http.StatusMethodNotAllowed, APIResponse{Success: false, Message: "Method not allowed"})
		return
	}
	if !requireScope(w, r, "image:read") {
		return
	}

	runtime := runtimeFromRequest(r.URL.Query().Get("type"))
	enabledSet := getEnabledImageSet()
	var subUser *config.SubUser
	var targetContainer *config.Container
	currentImageIDs := map[string]bool{}
	if isSubUserRequest(r) {
		subUser = subUserFromRequest(r)
		if identifier := r.URL.Query().Get("container"); identifier != "" {
			targetContainer = containerByIdentifier(identifier)
			if targetContainer == nil || !isContainerAllowedForRequest(r, identifier) {
				jsonResponse(w, http.StatusForbidden, APIResponse{Success: false, Message: "Access denied to this container"})
				return
			}
			currentImageIDs[targetContainer.Template] = true
		} else {
			for _, id := range subUserCurrentImageIDs(subUser) {
				currentImageIDs[id] = true
			}
		}
	}

	result := make([]map[string]string, 0)
	if runtime == config.VirtualizationKVM {
		if !hostKVMAvailable() {
			jsonResponse(w, http.StatusOK, APIResponse{Success: true, Data: result})
			return
		}
		for _, t := range kvm.GetImages() {
			if subUser != nil && !isImageAllowedForSubUser(subUser, targetContainer, t.ID) {
				continue
			}
			if downloaded, _ := kvm.ImageDownloadedInfo(t.ID); downloaded && (enabledSet[t.ID] || currentImageIDs[t.ID]) {
				result = append(result, map[string]string{
					"id": t.ID, "name": t.Name, "distro": t.Distro, "release": t.Release, "arch": t.Arch,
					"description": t.Description, "type": config.VirtualizationKVM, "desktop": t.Desktop,
				})
			}
		}
	} else {
		for _, t := range lxc.GetTemplates() {
			if subUser != nil && !isImageAllowedForSubUser(subUser, targetContainer, t.ID) {
				continue
			}
			if downloaded, _ := lxcTemplateDownloadedInfo(t); downloaded && (enabledSet[t.ID] || currentImageIDs[t.ID]) {
				result = append(result, map[string]string{
					"id": t.ID, "name": t.Name, "distro": t.Distro, "release": t.Release, "arch": t.Arch,
					"variant": t.Variant, "description": t.Description, "type": config.VirtualizationLXC,
				})
			}
		}
	}

	jsonResponse(w, http.StatusOK, APIResponse{Success: true, Data: result})
}

func isTemplateEnabledAndDownloaded(templateID string) bool {
	return isImageEnabledAndDownloaded(templateID, runtimeFromTemplateID(templateID))
}

func imageTemplateExists(templateID string) bool {
	return lxc.FindTemplate(templateID) != nil || kvm.FindImage(templateID) != nil
}

func isImageDownloadedForRuntime(templateID string, runtime string) bool {
	runtime = runtimeFromRequest(runtime)
	if runtime == config.VirtualizationKVM {
		if !hostKVMAvailable() {
			return false
		}
		image := kvm.FindImage(templateID)
		if image == nil {
			return false
		}
		downloaded, _ := kvm.ImageDownloadedInfo(image.ID)
		return downloaded
	}
	tmpl := lxc.FindTemplate(templateID)
	if tmpl == nil {
		return false
	}
	downloaded, _ := lxcTemplateDownloadedInfo(*tmpl)
	return downloaded
}

func isTemplateAvailableForRequest(r *http.Request, c *config.Container, templateID string, runtime string) bool {
	if isSubUserRequest(r) {
		if !isTemplateAllowedForRequest(r, c, templateID) {
			return false
		}
		if c != nil && c.Template == templateID {
			return isImageDownloadedForRuntime(templateID, runtime)
		}
	}
	return isImageEnabledAndDownloaded(templateID, runtime)
}

func isImageEnabledAndDownloaded(templateID string, runtime string) bool {
	runtime = runtimeFromRequest(runtime)
	if runtime == config.VirtualizationKVM {
		if !hostKVMAvailable() {
			return false
		}
		image := kvm.FindImage(templateID)
		if image == nil {
			return false
		}
		enabledSet := getEnabledImageSet()
		downloaded, _ := kvm.ImageDownloadedInfo(image.ID)
		return enabledSet[image.ID] && downloaded
	}
	tmpl := lxc.FindTemplate(templateID)
	if tmpl == nil {
		return false
	}
	enabledSet := getEnabledImageSet()
	downloaded, _ := lxcTemplateDownloadedInfo(*tmpl)
	return enabledSet[tmpl.ID] && downloaded
}

func hostKVMAvailable() bool {
	if runtime.GOARCH != "amd64" && runtime.GOARCH != "arm64" {
		return false
	}
	return fileExists("/dev/kvm") && commandExists("virsh") && commandExists(kvmQEMUCheckKey())
}

func ensureImageEnabled(id string) {
	// If the enabled list is empty, all templates are currently enabled by default.
	// We must populate the list with all template IDs first so that explicit toggles stick.
	if len(config.AppConfig.EnabledImages) == 0 {
		for _, t := range lxc.GetTemplates() {
			config.AppConfig.EnabledImages = append(config.AppConfig.EnabledImages, t.ID)
		}
		for _, t := range kvm.GetImages() {
			config.AppConfig.EnabledImages = append(config.AppConfig.EnabledImages, t.ID)
		}
		config.SaveConfig()
		return // Already contains all IDs including this one
	}
	found := false
	for _, eid := range config.AppConfig.EnabledImages {
		if eid == id {
			found = true
			break
		}
	}
	if !found {
		config.AppConfig.EnabledImages = append(config.AppConfig.EnabledImages, id)
		config.SaveConfig()
	}
}

func removeImageEnabled(id string) {
	// If the enabled list is empty, populate it first with all templates,
	// then remove the one being disabled.
	if len(config.AppConfig.EnabledImages) == 0 {
		for _, t := range lxc.GetTemplates() {
			if t.ID != id {
				config.AppConfig.EnabledImages = append(config.AppConfig.EnabledImages, t.ID)
			}
		}
		for _, t := range kvm.GetImages() {
			if t.ID != id {
				config.AppConfig.EnabledImages = append(config.AppConfig.EnabledImages, t.ID)
			}
		}
		config.SaveConfig()
		return
	}
	filtered := make([]string, 0, len(config.AppConfig.EnabledImages))
	for _, eid := range config.AppConfig.EnabledImages {
		if eid != id {
			filtered = append(filtered, eid)
		}
	}
	if len(filtered) != len(config.AppConfig.EnabledImages) {
		config.AppConfig.EnabledImages = filtered
		config.SaveConfig()
	}
}
