package kernelcat

import (
	"encoding/json"
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"sort"
)

// DiscoveredArtifacts is what DiscoverPaths returns: absolute paths to a
// kernel, an optional initrd, and an optional modules directory located
// under the staged output of make-*-kernel.sh (or an equivalent layout).
type DiscoveredArtifacts struct {
	KernelPath string
	InitrdPath string
	ModulesDir string
}

// metadataFile is the optional JSON a kernel-build script can drop
// alongside its staged output to point ReadLocal at specific filenames
// without guessing.
type metadataFile struct {
	KernelPath string `json:"kernel_path"`
	InitrdPath string `json:"initrd_path"`
	ModulesDir string `json:"modules_dir"`
}

// DiscoverPaths locates kernel / initrd / modules artifacts under fromDir.
// It prefers a metadata.json emitted by make-*-kernel.sh; otherwise it
// falls back to globbing boot/vmlinux-*, boot/vmlinuz-* (for Alpine),
// boot/initramfs-*, and the newest subdir under lib/modules/.
func DiscoverPaths(fromDir string) (DiscoveredArtifacts, error) {
	info, err := os.Stat(fromDir)
	if err != nil {
		return DiscoveredArtifacts{}, err
	}
	if !info.IsDir() {
		return DiscoveredArtifacts{}, fmt.Errorf("%s is not a directory", fromDir)
	}

	if paths, ok, err := discoverFromMetadata(fromDir); err != nil {
		return DiscoveredArtifacts{}, err
	} else if ok {
		return paths, nil
	}

	bootDir := filepath.Join(fromDir, "boot")
	kernel, err := latestMatch(bootDir, []string{"vmlinux-*", "vmlinuz-*"})
	if err != nil {
		return DiscoveredArtifacts{}, fmt.Errorf("locate kernel under %s: %w", bootDir, err)
	}
	initrd, err := latestMatch(bootDir, []string{"initramfs-*"})
	if err != nil && !errors.Is(err, os.ErrNotExist) {
		return DiscoveredArtifacts{}, fmt.Errorf("locate initrd under %s: %w", bootDir, err)
	}
	modules, err := latestSubdir(filepath.Join(fromDir, "lib", "modules"))
	if err != nil && !errors.Is(err, os.ErrNotExist) {
		return DiscoveredArtifacts{}, fmt.Errorf("locate modules under %s: %w", fromDir, err)
	}
	return DiscoveredArtifacts{
		KernelPath: kernel,
		InitrdPath: initrd,
		ModulesDir: modules,
	}, nil
}

func discoverFromMetadata(fromDir string) (DiscoveredArtifacts, bool, error) {
	data, err := os.ReadFile(filepath.Join(fromDir, "metadata.json"))
	if err != nil {
		if errors.Is(err, os.ErrNotExist) {
			return DiscoveredArtifacts{}, false, nil
		}
		return DiscoveredArtifacts{}, false, err
	}
	var meta metadataFile
	if err := json.Unmarshal(data, &meta); err != nil {
		return DiscoveredArtifacts{}, false, fmt.Errorf("parse metadata.json in %s: %w", fromDir, err)
	}
	kernel := absoluteOrAnchored(fromDir, meta.KernelPath)
	if kernel == "" {
		return DiscoveredArtifacts{}, false, nil
	}
	if _, err := os.Stat(kernel); err != nil {
		return DiscoveredArtifacts{}, false, fmt.Errorf("metadata.json references missing kernel %s: %w", kernel, err)
	}
	out := DiscoveredArtifacts{KernelPath: kernel}
	if meta.InitrdPath != "" {
		candidate := absoluteOrAnchored(fromDir, meta.InitrdPath)
		if _, err := os.Stat(candidate); err == nil {
			out.InitrdPath = candidate
		}
	}
	if meta.ModulesDir != "" {
		candidate := absoluteOrAnchored(fromDir, meta.ModulesDir)
		if info, err := os.Stat(candidate); err == nil && info.IsDir() {
			out.ModulesDir = candidate
		}
	}
	return out, true, nil
}

// absoluteOrAnchored returns path as-is if absolute; otherwise joins it to
// anchor. Empty input returns "".
func absoluteOrAnchored(anchor, path string) string {
	path = filepath.Clean(path)
	if path == "" || path == "." {
		return ""
	}
	if filepath.IsAbs(path) {
		return path
	}
	return filepath.Join(anchor, path)
}

// latestMatch returns the lexicographically latest file in dir matching any
// of patterns (filename globs, not full paths). Returns os.ErrNotExist if no
// match.
func latestMatch(dir string, patterns []string) (string, error) {
	if _, err := os.Stat(dir); err != nil {
		return "", err
	}
	entries, err := os.ReadDir(dir)
	if err != nil {
		return "", err
	}
	var matches []string
	for _, entry := range entries {
		if entry.IsDir() {
			continue
		}
		for _, pattern := range patterns {
			ok, _ := filepath.Match(pattern, entry.Name())
			if ok {
				matches = append(matches, entry.Name())
				break
			}
		}
	}
	if len(matches) == 0 {
		return "", os.ErrNotExist
	}
	sort.Strings(matches)
	return filepath.Join(dir, matches[len(matches)-1]), nil
}

// latestSubdir returns the lexicographically latest subdirectory of root.
// Returns os.ErrNotExist if root is missing or has no subdirs.
func latestSubdir(root string) (string, error) {
	if _, err := os.Stat(root); err != nil {
		return "", err
	}
	entries, err := os.ReadDir(root)
	if err != nil {
		return "", err
	}
	var dirs []string
	for _, entry := range entries {
		if entry.IsDir() {
			dirs = append(dirs, entry.Name())
		}
	}
	if len(dirs) == 0 {
		return "", os.ErrNotExist
	}
	sort.Strings(dirs)
	return filepath.Join(root, dirs[len(dirs)-1]), nil
}
