package info

import (
	"archive/zip"
	"bytes"
	"compress/gzip"
	_ "embed"
	"encoding/json"
	"fmt"
	"io"
	"io/fs"
	"path/filepath"
	"slices"
	"sort"
	"strings"

	"github.com/apex/log"
	"github.com/blacktop/ipsw/internal/utils"
	"github.com/blacktop/ipsw/pkg/devicetree"
	"github.com/blacktop/ipsw/pkg/plist"
	"github.com/blacktop/ipsw/pkg/xcode"
	"github.com/pkg/errors"
)

//go:embed data/procs.gz
var procsData []byte

var ErrorCryptexNotFound = errors.New("cryptex not found")

// Info in the info object
type Info struct {
	Plists      *plist.Plists
	DeviceTrees map[string]*devicetree.DeviceTree
}

type processors struct {
	Name          string
	Model         string
	Semiconductor string
	DieSize       string
	Transistors   string
	CPUISA        string
	CPU           string
	CPUID         string
	CPUCache      []string
	GPU           string
	AIAccelerator string
	Memory        string
	Introduced    string
	Devices       []string
}

type ProcessorDB []processors

func GetProcessorDB() (*ProcessorDB, error) {
	var ps ProcessorDB

	zr, err := gzip.NewReader(bytes.NewReader(procsData))
	if err != nil {
		return nil, err
	}
	defer zr.Close()

	if err := json.NewDecoder(zr).Decode(&ps); err != nil {
		return nil, fmt.Errorf("failed unmarshaling procs.gz data: %w", err)
	}

	return &ps, nil
}

func (p *ProcessorDB) GetProcessor(cpuid string) (*processors, error) {
	for _, proc := range *p {
		if strings.EqualFold(proc.CPUID, cpuid) {
			return &proc, nil
		}
	}
	return nil, fmt.Errorf("failed to find processor for '%s'", cpuid)
}

// getProcessors reads the processors from embedded JSON
func getProcessor(cpuid string) (processors, error) {
	var ps []processors

	zr, err := gzip.NewReader(bytes.NewReader(procsData))
	if err != nil {
		return processors{}, err
	}
	defer zr.Close()

	if err := json.NewDecoder(zr).Decode(&ps); err != nil {
		return processors{}, fmt.Errorf("failed unmarshaling procs.gz data: %w", err)
	}

	for _, p := range ps {
		if strings.EqualFold(p.CPUID, cpuid) {
			return p, nil
		}
	}

	return processors{}, fmt.Errorf("failed to find processor for %s", cpuid)
}

func (i *Info) String() string {
	var iStr strings.Builder
	var verextra string
	if i.Plists.OTAInfo != nil {
		verextra = fmt.Sprintf(" %s", i.Plists.OTAInfo.MobileAssetProperties.ProductVersionExtra)
	}
	if i.Plists.BuildManifest != nil {
		iStr.WriteString(fmt.Sprintf(
			"Version        = %s\n"+
				"BuildVersion   = %s\n"+
				"OS Type        = %s\n",
			i.Plists.BuildManifest.ProductVersion+verextra,
			i.Plists.BuildManifest.ProductBuildVersion,
			i.Plists.GetOSType(),
		))
	}
	if i.Plists.Restore != nil {
		foundFS := false
		if fsDMG, err := i.GetFileSystemOsDmg(); err == nil {
			foundFS = true
			iStr.WriteString(fmt.Sprintf("FileSystem     = %s\n", fsDMG))
		}
		if fsDMG, err := i.GetSystemOsDmg(); err == nil {
			iStr.WriteString(fmt.Sprintf("SystemOS       = %s\n", fsDMG))
		}
		if fsDMG, err := i.GetAppOsDmg(); err == nil {
			iStr.WriteString(fmt.Sprintf("AppOS          = %s\n", fsDMG))
		}
		if fsDMG, err := i.GetRosettaOsDmg(); err == nil {
			iStr.WriteString(fmt.Sprintf("RosettaOS      = %s\n", fsDMG))
		}
		if fsDMG, err := i.GetExclaveOSDmg(); err == nil {
			iStr.WriteString(fmt.Sprintf("ExclaveOS      = %s\n", fsDMG))
		}
		if ramDisk, err := i.GetRestoreRamDiskDmgs(); err == nil {
			iStr.WriteString(fmt.Sprintf("RestoreRamDisk = %s\n", ramDisk))
		}
		if !foundFS {
			if len(i.Plists.Restore.SystemRestoreImageFileSystems) > 0 {
				for file, fsType := range i.Plists.Restore.SystemRestoreImageFileSystems {
					iStr.WriteString(fmt.Sprintf("FileSystem     = %s (Type: %s)\n", file, fsType))
				}
			}
		}
	}
	if i.Plists.OTAInfo != nil {
		if len(i.Plists.OTAInfo.MobileAssetProperties.RestoreVersion) > 0 {
			iStr.WriteString(fmt.Sprintf("RestoreVersion = %s\n", i.Plists.OTAInfo.MobileAssetProperties.RestoreVersion))
		}
		if len(i.Plists.OTAInfo.MobileAssetProperties.PrerequisiteBuild) > 0 {
			iStr.WriteString(fmt.Sprintf("PrereqBuild    = %s\n", i.Plists.OTAInfo.MobileAssetProperties.PrerequisiteBuild))
		}
		if i.Plists.OTAInfo.MobileAssetProperties.SplatOnly {
			iStr.WriteString("IsRSR          = ✅\n")
		}
	}
	if len(i.DeviceTrees) > 0 {
		kcs := i.Plists.BuildManifest.GetKernelCaches()
		bls := i.Plists.BuildManifest.GetBootLoaders()
		iStr.WriteString("\nDevices\n")
		iStr.WriteString("-------\n")
		for _, dtree := range i.DeviceTrees {
			dt, _ := dtree.Summary()
			prodName := dt.ProductName
			if len(prodName) == 0 {
				devices, err := xcode.GetDevices()
				if err == nil {
					for _, device := range devices {
						if device.ProductType == dt.ProductType {
							prodName = device.ProductDescription
							break
						}
					}
				} else {
					prodName = dt.ProductType
				}
			}
			iStr.WriteString(fmt.Sprintf("\n%s\n", prodName))
			iStr.WriteString(fmt.Sprintf(" > %s_%s_%s\n", dt.ProductType, strings.ToUpper(dt.BoardConfig), i.Plists.BuildManifest.ProductBuildVersion))
			if !dt.Timestamp.IsZero() {
				iStr.WriteString(fmt.Sprintf("   - TimeStamp: %s\n", dt.Timestamp.Format("02 Jan 2006 15:04:05 MST")))
			}
			if len(kcs[strings.ToLower(dt.BoardConfig)]) > 0 {
				iStr.WriteString(fmt.Sprintf("   - KernelCache: %s\n", strings.Join(kcs[strings.ToLower(dt.BoardConfig)], ", ")))
			}
			if cpu := i.GetCPU(dt.BoardConfig); len(cpu) > 0 {
				iStr.WriteString(fmt.Sprintf("   - %s\n", cpu))
			}
			if len(bls[strings.ToLower(dt.BoardConfig)]) > 0 {
				iStr.WriteString("   - BootLoaders\n")
				for _, bl := range bls[strings.ToLower(dt.BoardConfig)] {
					iStr.WriteString(fmt.Sprintf("       * %s\n", filepath.Base(bl)))
				}
			}
		}
	} else {
		if i.Plists.BuildManifest != nil {
			iStr.WriteString("\nDevices\n")
			iStr.WriteString("-------\n")
			for _, dev := range i.Plists.BuildManifest.SupportedProductTypes {
				iStr.WriteString(fmt.Sprintf(" > %s_%s\n", dev, i.Plists.BuildManifest.ProductBuildVersion))
			}
		}
	}
	return iStr.String()
}

type InfoJSON struct {
	Type    string `json:"type,omitempty"`
	Version string `json:"version,omitempty"`
	Build   string `json:"build,omitempty"`
	OS      string `json:"os,omitempty"`
	Devices any    `json:"devices,omitempty"`
	Error   string `json:"error,omitempty"`
}

func (i *Info) ToJSON() InfoJSON {
	if i.Plists.BuildIdentities == nil {
		return InfoJSON{Error: "no BuildManifest.plist found"}
	}
	return InfoJSON{
		Type:    i.Plists.Type,
		Version: i.Plists.BuildManifest.ProductVersion,
		Build:   i.Plists.BuildManifest.ProductBuildVersion,
		OS:      i.Plists.GetOSType(),
		Devices: func() any {
			if len(i.DeviceTrees) > 0 {
				var devs []struct {
					Name      string `json:"name,omitempty"`
					Product   string `json:"product,omitempty"`
					Board     string `json:"board,omitempty"`
					Timestamp string `json:"timestamp,omitempty"`
					CPU       string `json:"cpu,omitempty"`
				}
				for _, dtree := range i.DeviceTrees {
					dt, _ := dtree.Summary()
					devs = append(devs, struct {
						Name      string `json:"name,omitempty"`
						Product   string `json:"product,omitempty"`
						Board     string `json:"board,omitempty"`
						Timestamp string `json:"timestamp,omitempty"`
						CPU       string `json:"cpu,omitempty"`
					}{
						Name:      dt.ProductName,
						Product:   dt.ProductType,
						Board:     dt.BoardConfig,
						Timestamp: dt.Timestamp.Format("02 Jan 2006 15:04:05 MST"),
						CPU:       i.GetCPU(dt.BoardConfig),
					})
				}
				return devs
			} else {
				return i.Plists.MobileAssetProperties.SupportedDevices
			}
		}(),
	}
}

// getCryptexDmg returns the DMG path declared under the given BuildManifest key (e.g. "Cryptex1,AppOS")
func (i *Info) getCryptexDmg(key, name string) (string, error) {
	if i.Plists == nil || i.Plists.BuildManifest == nil {
		return "", fmt.Errorf("no BuildManifest.plist found")
	}
	var dmgs []string
	for _, bi := range i.Plists.BuildIdentities {
		if cryptex, ok := bi.Manifest[key]; ok {
			dmgs = append(dmgs, cryptex.Info["Path"].(string))
		}
	}
	dmgs = utils.Unique(dmgs)
	switch len(dmgs) {
	case 0:
		return "", fmt.Errorf("no %s DMG found: %w", name, ErrorCryptexNotFound)
	case 1:
		return dmgs[0], nil
	default:
		return "", fmt.Errorf("multiple %s DMGs found", name)
	}
}

// GetAppOsDmg returns the name of the AppOS dmg
func (i *Info) GetAppOsDmg() (string, error) {
	return i.getCryptexDmg("Cryptex1,AppOS", "AppOS")
}

// GetRosettaOsDmg returns the name of the RosettaOS dmg (macOS 27+ cryptex)
func (i *Info) GetRosettaOsDmg() (string, error) {
	return i.getCryptexDmg("Cryptex1,RosettaOS", "RosettaOS")
}

// GetSystemOsDmg returns the name of the SystemOS dmg (the one with the dyld_shared_cache(s))
func (i *Info) GetSystemOsDmg() (string, error) {
	var dmgs []string
	if i.Plists != nil && i.Plists.BuildManifest != nil {
		for _, bi := range i.Plists.BuildIdentities {
			if sysOS, ok := bi.Manifest["Cryptex1,SystemOS"]; ok {
				return sysOS.Info["Path"].(string), nil
			}
		}
		dmgs = utils.Unique(dmgs)
		if len(dmgs) == 0 {
			return "", fmt.Errorf("no SystemOS DMG found: %w", ErrorCryptexNotFound)
		} else if len(dmgs) == 1 {
			return dmgs[0], nil
		} else {
			return "", fmt.Errorf("multiple SystemOS DMGs found")
		}
	}
	return "", fmt.Errorf("no BuildManifest.plist found")
}

// GetFileSystemOsDmg returns the name of the file system dmg
func (i *Info) GetFileSystemOsDmg() (string, error) {
	var dmgs []string
	if i.Plists != nil && i.Plists.BuildManifest != nil {
		for _, bi := range i.Plists.BuildIdentities {
			if fsOS, ok := bi.Manifest["OS"]; ok {
				// log.Debugf("Found: %s", fsOS.Info["Path"].(string))
				if !strings.Contains(bi.Info.Variant, "Recovery") {
					dmgs = append(dmgs, fsOS.Info["Path"].(string))
				}
			}
		}
		dmgs = utils.Unique(dmgs)
		if len(dmgs) == 0 {
			return "", fmt.Errorf("no filesystem DMG found: %w", ErrorCryptexNotFound)
		} else if len(dmgs) == 1 {
			return dmgs[0], nil
		} else {
			return "", fmt.Errorf("multiple filesystem DMGs found")
		}
	} else if i.Plists != nil && i.Plists.Restore != nil {
		if dmg, ok := i.Plists.Restore.SystemRestoreImages["User"]; ok {
			return dmg, nil
		}
		return "", fmt.Errorf("no BuildManifest.plist AND no SystemRestoreImages (used in older IPSWs) found")
	}
	return "", fmt.Errorf("no BuildManifest.plist found")
}

// GetRestoreRamDiskDmgs returns the name of the RestoreRamDisk dmg
func (i *Info) GetRestoreRamDiskDmgs() ([]string, error) {
	var dmgs []string
	if i.Plists != nil && i.Plists.BuildManifest != nil {
		for _, bi := range i.Plists.BuildIdentities {
			if rrdisk, ok := bi.Manifest["RestoreRamDisk"]; ok {
				dmgs = append(dmgs, rrdisk.Info["Path"].(string))
			}
		}
		dmgs = utils.Unique(dmgs)
	}
	if len(dmgs) > 0 {
		return dmgs, nil
	}
	return nil, fmt.Errorf("no RestoreRamDisk DMG found")
}

// GetRestoreRamDiskDmgByIdent returns the RestoreRamDisk DMG path matching a given identity hint.
// The ident string is a substring of Info.Variant (e.g. "Customer Erase", "Customer Upgrade") [case-insensitive]
func (i *Info) GetRestoreRamDiskDmgByIdent(ident string) (string, error) {
	if i.Plists == nil || i.Plists.BuildManifest == nil {
		return "", fmt.Errorf("no BuildManifest.plist found")
	}
	if len(ident) == 0 {
		return "", fmt.Errorf("ident is empty")
	}
	identLower := strings.ToLower(ident)
	var matches []string
	var variants []string
	for _, bi := range i.Plists.BuildIdentities {
		rrdisk, ok := bi.Manifest["RestoreRamDisk"]
		if !ok {
			continue
		}
		variants = append(variants, bi.Info.Variant)
		path := rrdisk.Info["Path"].(string)
		if len(path) == 0 {
			continue
		}
		if len(bi.Info.Variant) > 0 && strings.Contains(strings.ToLower(bi.Info.Variant), identLower) {
			matches = append(matches, path)
			continue
		}
	}
	matches = utils.Unique(matches)
	switch len(matches) {
	case 0:
		return "", fmt.Errorf("no RestoreRamDisk DMG found for ident '%s': found %v", ident, strings.Join(variants, ", "))
	case 1:
		return matches[0], nil
	default:
		return "", fmt.Errorf("multiple RestoreRamDisk DMGs matched ident '%s': %v", ident, strings.Join(variants, ", "))
	}
}

// GetRestoreRamDiskDmg returns the best RestoreRamDisk DMG path.
// If ident is non-empty, it finds a matching identity. Otherwise, it prefers
// Erase, then Update, then falls back to the first available DMG.
func (i *Info) GetRestoreRamDiskDmg(ident string) (string, error) {
	if len(ident) > 0 {
		return i.GetRestoreRamDiskDmgByIdent(ident)
	}
	// Prefer Erase -> Update -> first
	if p, err := i.GetRestoreRamDiskDmgByIdent("Erase"); err == nil {
		return p, nil
	}
	if p, err := i.GetRestoreRamDiskDmgByIdent("Update"); err == nil {
		return p, nil
	}
	dmgs, err := i.GetRestoreRamDiskDmgs()
	if err != nil {
		return "", fmt.Errorf("failed to get RestoreRamDisk DMGs: %w", err)
	}
	if len(dmgs) == 0 {
		return "", fmt.Errorf("no RestoreRamDisk DMG found")
	}
	return dmgs[0], nil
}

func (i *Info) GetExclaveOSDmg() (string, error) {
	return i.getCryptexDmg("Ap,ExclaveOS", "ExclaveOS")
}

func (i *Info) IsMacOS() bool {
	for _, dev := range i.Plists.BuildManifest.SupportedProductTypes {
		if strings.Contains(dev, "Mac") {
			return true
		}
	}
	return false
}

// GetOsDmg returns the name of the OS dmg
func (i *Info) GetCPU(board string) string {
	if i.Plists.Restore != nil {
		for _, device := range i.Plists.Restore.DeviceMap {
			if strings.EqualFold(device.BoardConfig, board) {
				if proc, err := getProcessor(device.Platform); err == nil {
					return fmt.Sprintf("CPU: %s (%s), ID: %s", proc.Name, proc.CPUISA, device.Platform)
				} else {
					return fmt.Sprintf("ID: %s", device.Platform)
				}
			}
		}
	} else if i.Plists.BuildManifest != nil {
		for _, ident := range i.Plists.BuildIdentities {
			if strings.EqualFold(ident.Info.DeviceClass, board) {
				plat := "t" + strings.TrimPrefix(ident.ApChipID, "0x")
				if proc, err := getProcessor(plat); err == nil {
					return fmt.Sprintf("CPU: %s (%s), ID: %s", proc.Name, proc.CPUISA, plat)
				} else {
					return fmt.Sprintf("AP ChipID: %s", ident.ApChipID)
				}
			}
		}
	}
	return ""
}

// GetFolder returns a folder name for all the devices included in an IPSW
func (i *Info) GetFolder(device ...string) (string, error) {
	if i.Plists.BuildManifest == nil {
		if i.Plists.Type == "OTA" && i.Plists.OTAInfo.CFBundleName == "SimulatorRuntimeAsset" {
			typ, found := strings.CutPrefix(i.Plists.OTAInfo.CFBundleIdentifier, "com.apple.MobileAsset.")
			if !found {
				typ = "Simulator"
			}
			return fmt.Sprintf("%s_%s_%s",
				i.Plists.OTAInfo.MobileAssetProperties.SimulatorVersion,
				i.Plists.OTAInfo.MobileAssetProperties.Build,
				typ,
			), nil
		}
		return "", fmt.Errorf("no BuildManifest.plist found")
	}

	var dev string
	if len(device) > 0 && len(device[0]) > 0 {
		dev = device[0]
	}

	var devs []string
	if len(i.DeviceTrees) > 0 {
		for _, dtree := range i.DeviceTrees {
			dt, err := dtree.Summary()
			if err != nil {
				log.Fatal(err.Error())
			}
			devs = append(devs, dt.ProductType)
		}

		devs = utils.SortDevices(utils.Unique(devs))

		if len(dev) > 0 {
			if slices.Contains(devs, dev) {
				return fmt.Sprintf("%s__%s", i.Plists.BuildManifest.ProductBuildVersion, dev), nil
			} else {
				return "", fmt.Errorf("device '%s' not found in IPSW/OTA", dev)
			}
		}

		return fmt.Sprintf("%s__%s", i.Plists.BuildManifest.ProductBuildVersion, getAbbreviatedDevListFolder(devs)), nil
	}

	sort.Strings(i.Plists.BuildManifest.SupportedProductTypes)
	return fmt.Sprintf("%s__%s", i.Plists.BuildManifest.ProductBuildVersion, getAbbreviatedDevListFolder(i.Plists.BuildManifest.SupportedProductTypes)), nil
}

// GetFolders returns a list of the IPSW name folders
func (i *Info) GetFolders() ([]string, error) {
	if i.Plists.BuildManifest == nil {
		return nil, fmt.Errorf("no BuildManifest.plist found")
	}
	var folders []string
	if len(i.DeviceTrees) > 0 {
		for _, dtree := range i.DeviceTrees {
			dt, _ := dtree.Summary()
			folders = append(folders, fmt.Sprintf("%s_%s_%s", dt.ProductType, strings.ToUpper(dt.BoardConfig), i.Plists.BuildManifest.ProductBuildVersion))
		}
		return folders, nil
	}
	return nil, fmt.Errorf("no devices found")
}

// GetFolderForFile returns a list of the IPSW name folders for a given file
func (i *Info) GetFolderForFile(fileName string) (string, error) {
	if i.Plists.BuildManifest == nil {
		return "", fmt.Errorf("no BuildManifest.plist found")
	}
	if len(i.DeviceTrees) > 0 {
		files := i.getManifestPaths()
		for _, dtree := range i.DeviceTrees {
			dt, _ := dtree.Summary()
			for _, file := range files[strings.ToLower(dt.BoardConfig)] {
				if strings.Contains(fileName, filepath.Base(file)) {
					return fmt.Sprintf("%s_%s_%s", dt.ProductType, strings.ToUpper(dt.BoardConfig), i.Plists.BuildManifest.ProductBuildVersion), nil
				}
			}
		}
	}
	return "", fmt.Errorf("no devices found")
}

func (i *Info) getManifestPaths() map[string][]string {

	files := make(map[string][]string, len(i.Plists.BuildIdentities))

	for _, bID := range i.Plists.BuildIdentities {
		for _, manifest := range bID.Manifest {
			if len(manifest.Info["Path"].(string)) > 0 {
				files[bID.Info.DeviceClass] = append(files[bID.Info.DeviceClass], manifest.Info["Path"].(string))
			}
		}
	}

	return files
}

type folder struct {
	Name         string
	KernelCaches []string
}

func (i *Info) getFolders() ([]folder, error) {
	if i.Plists.BuildManifest == nil {
		return nil, fmt.Errorf("no BuildManifest.plist found")
	}
	if len(i.DeviceTrees) > 0 {
		var fs []folder
		kcs := i.Plists.BuildManifest.GetKernelCaches()
		for _, dtree := range i.DeviceTrees {
			dt, _ := dtree.Summary()
			fs = append(fs, folder{
				Name:         fmt.Sprintf("%s_%s_%s", dt.ProductType, strings.ToUpper(dt.BoardConfig), i.Plists.BuildManifest.ProductBuildVersion),
				KernelCaches: kcs[strings.ToLower(dt.BoardConfig)],
			})
		}
		return fs, nil
	}
	return nil, fmt.Errorf("no devices found")
}

// GetKernelCacheFolders returns the folders belonging to a KernelCache
func (i *Info) GetKernelCacheFolders(kc string) ([]string, error) {
	var folders []string
	fds, err := i.getFolders()
	if err != nil {
		return nil, fmt.Errorf("failed to get folders: %v", err)
	}
	for _, folder := range fds {
		for _, kcache := range folder.KernelCaches {
			if strings.HasSuffix(kc, kcache) {
				folders = append(folders, folder.Name)
			}
		}
	}
	return folders, nil
}

// GetKernelCacheFileName returns a short new kernelcache name including all the supported devices
func (i *Info) GetKernelCacheFileName(kc string) string {
	devices := i.GetDevicesForKernelCache(filepath.Base(kc))
	if i.kernelCacheDeviceListCollides(filepath.Base(kc), devices) {
		return filepath.Base(kc)
	}
	devList := getAbbreviatedDevList(devices)
	if len(devList) == 0 {
		return filepath.Base(kc)
	}
	return fmt.Sprintf("%s.%s", strings.TrimSuffix(filepath.Base(kc), filepath.Ext(kc)), devList)
}

// GetDevicesForKernelCache returns a sorted array of devices that support the kernelcache
func (i *Info) GetDevicesForKernelCache(kc string) []string {
	var devices []string
	if i.Plists == nil || i.Plists.BuildManifest == nil {
		return nil
	}

	for bconf, kcache := range i.Plists.BuildManifest.GetKernelCaches() {
		if utils.StrSliceHas(kcache, filepath.Base(kc)) {
			var matchedDeviceTree bool
			for _, dtree := range i.DeviceTrees {
				if dtree == nil {
					continue
				}
				dt, err := dtree.Summary()
				if err != nil {
					continue
				}
				if strings.EqualFold(bconf, dt.BoardConfig) {
					matchedDeviceTree = true
					devices = append(devices, dt.ProductType)
				}
			}
			if !matchedDeviceTree {
				devices = append(devices, i.getDevicesForBuildIdentityKernelCache(filepath.Base(kc), bconf)...)
			}
		}
	}

	return utils.SortDevices(utils.Unique(devices))
}

// GetKernelCacheForDevice returns the kernelcache path(s) for a given device ProductType (e.g., "Mac16,8", "iPhone17,1")
func (i *Info) GetKernelCacheForDevice(device string) []string {
	if i.Plists == nil || i.Plists.BuildManifest == nil {
		return nil
	}

	// Find the BoardConfig for this device
	var boardConfig string
	for _, dtree := range i.DeviceTrees {
		if dtree == nil {
			continue
		}
		dt, err := dtree.Summary()
		if err != nil {
			continue
		}
		if dt.ProductType == device {
			boardConfig = strings.ToLower(dt.BoardConfig)
			break
		}
	}
	if boardConfig != "" {
		// Get kernelcaches for this BoardConfig
		kcs := i.Plists.BuildManifest.GetKernelCaches()
		return kcs[boardConfig]
	}

	return i.getKernelCachesForDeviceFromBuildManifest(device)
}

func (i *Info) getKernelCachesForDeviceFromBuildManifest(device string) []string {
	if i.Plists == nil || i.Plists.BuildManifest == nil {
		return nil
	}

	var kcs []string
	singleDeviceIPSW := len(i.Plists.BuildManifest.SupportedProductTypes) == 1 &&
		strings.EqualFold(i.Plists.BuildManifest.SupportedProductTypes[0], device)
	for _, ident := range i.Plists.BuildManifest.BuildIdentities {
		if !singleDeviceIPSW && !strings.EqualFold(ident.ApProductType, device) {
			continue
		}
		kernel, ok := ident.Manifest["KernelCache"]
		if !ok {
			continue
		}
		path, ok := getIdentityManifestPath(kernel)
		if !ok {
			continue
		}
		kcs = append(kcs, path)
	}

	return utils.Unique(kcs)
}

func (i *Info) getDevicesForBuildIdentityKernelCache(kc, deviceClass string) []string {
	if i.Plists == nil || i.Plists.BuildManifest == nil {
		return nil
	}

	var devices []string
	for _, ident := range i.Plists.BuildManifest.BuildIdentities {
		if !strings.EqualFold(ident.Info.DeviceClass, deviceClass) {
			continue
		}
		kernel, ok := ident.Manifest["KernelCache"]
		if !ok {
			continue
		}
		path, ok := getIdentityManifestPath(kernel)
		if !ok || filepath.Base(path) != filepath.Base(kc) {
			continue
		}
		if len(ident.ApProductType) > 0 {
			devices = append(devices, ident.ApProductType)
		} else if len(i.Plists.BuildManifest.SupportedProductTypes) == 1 {
			devices = append(devices, i.Plists.BuildManifest.SupportedProductTypes[0])
		}
	}

	return devices
}

func (i *Info) kernelCacheDeviceListCollides(kc string, devices []string) bool {
	if i.Plists == nil || i.Plists.BuildManifest == nil || len(devices) == 0 {
		return false
	}

	seen := make(map[string]struct{})
	for _, kcaches := range i.Plists.BuildManifest.GetKernelCaches() {
		for _, otherKC := range kcaches {
			otherBase := filepath.Base(otherKC)
			if otherBase == filepath.Base(kc) {
				continue
			}
			if _, ok := seen[otherBase]; ok {
				continue
			}
			seen[otherBase] = struct{}{}
			if slices.Equal(devices, i.GetDevicesForKernelCache(otherBase)) {
				return true
			}
		}
	}

	return false
}

func getIdentityManifestPath(manifest plist.IdentityManifest) (string, bool) {
	if manifest.Info == nil {
		return "", false
	}
	path, ok := manifest.Info["Path"].(string)
	if !ok || len(path) == 0 {
		return "", false
	}
	return path, true
}

func getAbbreviatedDevList(devices []string) string {
	var devList strings.Builder

	if len(devices) == 0 {
		return ""
	} else if len(devices) == 1 {
		return devices[0]
	}

	currentDev := devices[0]
	devPrefix := strings.Split(currentDev, ",")[0]
	devList.WriteString(currentDev)

	for _, dev := range devices[1:] {
		if strings.HasPrefix(dev, devPrefix) {
			devList.WriteString(fmt.Sprintf("_%s", strings.Split(dev, ",")[1]))
		} else {
			currentDev = dev
			devPrefix = strings.Split(currentDev, ",")[0]
			devList.WriteString("_" + currentDev)
		}
	}

	return devList.String()
}

func getAbbreviatedDevListFolder(devices []string) string {
	var devList strings.Builder

	if len(devices) == 0 {
		return ""
	} else if len(devices) == 1 {
		return devices[0]
	} else if strings.Contains(devices[0], "Mac") {
		return "MacOS"
	}

	currentDev := devices[0]
	devPrefix := strings.Split(currentDev, ",")[0]
	devList.WriteString(currentDev)

	for _, dev := range devices[1:] {
		if strings.HasPrefix(dev, devPrefix) {
			devList.WriteString(fmt.Sprintf("_%s", strings.Split(dev, ",")[1]))
		} else {
			currentDev = dev
			devPrefix = strings.Split(currentDev, ",")[0]
			devList.WriteString("_" + currentDev)
		}
	}

	return devList.String()
}

// Parse parses plist files in a local ipsw file
func Parse(ipswPath string, keys ...string) (*Info, error) {
	var err error

	i := &Info{}

	i.Plists, err = plist.Parse(ipswPath)
	if err != nil {
		return nil, fmt.Errorf("failed to parse plists: %v", err)
	}
	i.DeviceTrees, err = devicetree.Parse(ipswPath, keys...)
	if err != nil {
		if errors.Is(err, devicetree.ErrEncryptedDeviceTree) { // FIXME: this is a hack to avoid stopping the parsing of the metadata info
			log.Debug(err.Error())
		} else {
			log.Errorf("failed to parse devicetree: %v", err)
		}
	}

	return i, nil
}

// ParseZipFiles parses plist files and devicetree in a remote zip file
func ParseZipFiles(files []*zip.File, keys ...string) (*Info, error) {
	var err error

	i := &Info{}

	i.Plists, err = plist.ParseZipFiles(files)
	if err != nil {
		return nil, fmt.Errorf("failed to parse plists: %v", err)
	}
	i.DeviceTrees, err = devicetree.ParseZipFiles(files, keys...)
	if err != nil {
		if errors.Is(err, devicetree.ErrEncryptedDeviceTree) { // FIXME: this is a hack to avoid stopping the parsing of the metadata info
			log.Debug(err.Error())
		} else {
			log.Errorf("failed to parse devicetree: %v", err)
		}
	}

	return i, nil
}

func ParseOTAFiles(files []fs.File) (*Info, error) {
	var err error

	i := &Info{}

	i.DeviceTrees = make(map[string]*devicetree.DeviceTree)

	i.Plists, err = plist.ParsePlistFiles(files)
	if err != nil {
		return nil, fmt.Errorf("failed to parse plists: %v", err)
	}
	for _, f := range files {
		fi, err := f.Stat()
		if err != nil {
			return nil, fmt.Errorf("failed to get file info: %v", err)
		}
		if filepath.Ext(fi.Name()) == ".im4p" {
			dat, err := io.ReadAll(f)
			if err != nil {
				return nil, fmt.Errorf("failed to read file: %v", err)
			}
			dt, err := devicetree.ParseImg4Data(dat)
			if err != nil {
				if errors.Is(err, devicetree.ErrEncryptedDeviceTree) { // FIXME: this is a hack to avoid stopping the parsing of the metadata info
					log.Debug(err.Error())
				} else {
					log.Errorf("failed to parse devicetree: %v", err)
				}
			}
			i.DeviceTrees[fi.Name()] = dt
		}
	}

	return i, nil
}
