package platform

import (
	"encoding/json"
	"fmt"
	"io"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
)

func (s *System) IsLiveMediaInRAM() bool {
	out, err := exec.Command("findmnt", "-n", "-o", "FSTYPE", "/run/live/medium").Output()
	if err != nil {
		return toramActive()
	}
	return strings.TrimSpace(string(out)) == "tmpfs"
}

func (s *System) RunInstallToRAM(logFunc func(string)) error {
	log := func(msg string) {
		if logFunc != nil {
			logFunc(msg)
		}
	}

	if s.IsLiveMediaInRAM() {
		log("Already running from RAM — installation media can be safely disconnected.")
		return nil
	}

	squashfsFiles, err := filepath.Glob("/run/live/medium/live/*.squashfs")
	if err != nil || len(squashfsFiles) == 0 {
		return fmt.Errorf("no squashfs files found in /run/live/medium/live/")
	}

	free := freeMemBytes()
	var needed int64
	for _, sf := range squashfsFiles {
		fi, err2 := os.Stat(sf)
		if err2 != nil {
			return fmt.Errorf("stat %s: %v", sf, err2)
		}
		needed += fi.Size()
	}
	const headroom = 256 * 1024 * 1024
	if free > 0 && needed+headroom > free {
		return fmt.Errorf("insufficient RAM: need %s, available %s",
			humanBytes(needed+headroom), humanBytes(free))
	}

	dstDir := "/dev/shm/bee-live"
	if err := os.MkdirAll(dstDir, 0755); err != nil {
		return fmt.Errorf("create tmpfs dir: %v", err)
	}

	for _, sf := range squashfsFiles {
		base := filepath.Base(sf)
		dst := filepath.Join(dstDir, base)
		log(fmt.Sprintf("Copying %s to RAM...", base))
		if err := copyFileLarge(sf, dst, log); err != nil {
			return fmt.Errorf("copy %s: %v", base, err)
		}
		log(fmt.Sprintf("Copied %s.", base))

		loopDev, err := findLoopForFile(sf)
		if err != nil {
			log(fmt.Sprintf("Loop device for %s not found (%v) — skipping re-association.", base, err))
			continue
		}
		if err := reassociateLoopDevice(loopDev, dst); err != nil {
			log(fmt.Sprintf("Warning: could not re-associate %s → %s: %v", loopDev, dst, err))
		} else {
			log(fmt.Sprintf("Loop device %s now backed by RAM copy.", loopDev))
		}
	}

	log("Copying remaining medium files...")
	if err := cpDir("/run/live/medium", dstDir, log); err != nil {
		log(fmt.Sprintf("Warning: partial copy: %v", err))
	}
	if err := exec.Command("mount", "--bind", dstDir, "/run/live/medium").Run(); err != nil {
		log(fmt.Sprintf("Warning: rebind /run/live/medium failed: %v", err))
	}

	log("Done. Installation media can be safely disconnected.")
	return nil
}

func copyFileLarge(src, dst string, logFunc func(string)) error {
	in, err := os.Open(src)
	if err != nil {
		return err
	}
	defer in.Close()
	fi, err := in.Stat()
	if err != nil {
		return err
	}
	out, err := os.Create(dst)
	if err != nil {
		return err
	}
	defer out.Close()
	total := fi.Size()
	var copied int64
	buf := make([]byte, 4*1024*1024)
	for {
		n, err := in.Read(buf)
		if n > 0 {
			if _, werr := out.Write(buf[:n]); werr != nil {
				return werr
			}
			copied += int64(n)
			if logFunc != nil && total > 0 {
				pct := int(float64(copied) / float64(total) * 100)
				logFunc(fmt.Sprintf("  %s / %s (%d%%)", humanBytes(copied), humanBytes(total), pct))
			}
		}
		if err == io.EOF {
			break
		}
		if err != nil {
			return err
		}
	}
	return out.Sync()
}

func cpDir(src, dst string, logFunc func(string)) error {
	return filepath.Walk(src, func(path string, fi os.FileInfo, err error) error {
		if err != nil {
			return nil
		}
		rel, _ := filepath.Rel(src, path)
		target := filepath.Join(dst, rel)
		if fi.IsDir() {
			return os.MkdirAll(target, fi.Mode())
		}
		if strings.HasSuffix(path, ".squashfs") {
			return nil
		}
		if _, err := os.Stat(target); err == nil {
			return nil
		}
		return copyFileLarge(path, target, nil)
	})
}

func findLoopForFile(backingFile string) (string, error) {
	out, err := exec.Command("losetup", "--list", "--json").Output()
	if err != nil {
		return "", err
	}
	var result struct {
		Loopdevices []struct {
			Name     string `json:"name"`
			BackFile string `json:"back-file"`
		} `json:"loopdevices"`
	}
	if err := json.Unmarshal(out, &result); err != nil {
		return "", err
	}
	for _, dev := range result.Loopdevices {
		if dev.BackFile == backingFile {
			return dev.Name, nil
		}
	}
	return "", fmt.Errorf("no loop device found for %s", backingFile)
}

func reassociateLoopDevice(loopDev, newFile string) error {
	if err := exec.Command("losetup", "--replace", loopDev, newFile).Run(); err == nil {
		return nil
	}
	return loopChangeFD(loopDev, newFile)
}
