package kernelcache

import (
	"bytes"
	"encoding/json"
	"fmt"
	"regexp"
	"strings"

	"github.com/apex/log"
	"github.com/blacktop/go-macho"
	"github.com/blacktop/go-plist"
	"github.com/blacktop/ipsw/internal/utils"
	"github.com/dominikbraun/graph"
)

func InspectKM(m *macho.File, filter string, explicitOnly, asJSON bool) (string, error) {
	if sec := m.Section("__PRELINK_INFO", "__info"); sec != nil {
		dat, err := sec.Data()
		if err != nil {
			return "", fmt.Errorf("failed to read __PRELINK_INFO.__info section: %v", err)
		}

		var prelink PrelinkInfo
		if err := plist.NewDecoder(bytes.NewReader(dat)).Decode(&prelink); err != nil {
			return "", fmt.Errorf("failed to decode prelink info: %v", err)
		}

		if asJSON {
			jsonPL, err := json.Marshal(prelink)
			if err != nil {
				return "", fmt.Errorf("failed to marshal prelink info: %v", err)
			}
			return string(jsonPL), nil
		}

		var whitelist []string

		if len(filter) == 0 { // NO FILTER (add all bundles to whitelist)
			for _, bundle := range prelink.PrelinkInfoDictionary {
				if explicitOnly {
					whitelist = append(whitelist, fmt.Sprintf("-b %s", bundle.ID))
				} else {
					whitelist = append(whitelist, bundle.ID)
				}
			}
		} else {
			re, err := regexp.Compile(filter)
			if err != nil {
				return "", fmt.Errorf("failed to compile filter regex: %v", err)
			}

			var filterlist []string
			var blacklist []string

			for _, bundle := range prelink.PrelinkInfoDictionary {
				if re.MatchString(bundle.ID) {
					filterlist = append(filterlist, bundle.ID)
				}
			}

			g := graph.New(graph.StringHash, graph.Directed(), graph.PreventCycles())

			// generate dependency graph
			for _, bundle := range prelink.PrelinkInfoDictionary {
				if len(bundle.OSBundleLibraries) > 0 {
					g.AddVertex(bundle.ID)
					for dependency := range bundle.OSBundleLibraries {
						g.AddVertex(dependency)
						if err := g.AddEdge(bundle.ID, dependency); err != nil {
							return "", fmt.Errorf("failed to add edge: %v", err)
						}
					}
				}
			}

			blacklist = append(blacklist, filterlist...)

			// blacklist all bundles that dependacy paths contain filtered bundles
			for _, skip := range filterlist {
				for _, bundle := range prelink.PrelinkInfoDictionary {
					path, err := graph.ShortestPath(g, bundle.ID, skip)
					if err != nil {
						continue
					}
					if len(path) > 0 {
						blacklist = append(blacklist, bundle.ID)
					}
				}
			}

			// whitelist all bundles that are not blacklisted
			for _, bundle := range prelink.PrelinkInfoDictionary {
				if utils.StrSliceContains(blacklist, bundle.ID) {
					utils.Indent(log.WithFields(log.Fields{
						"bundle": bundle.ID,
					}).Info, 2)("Filtered")
					continue
				}
				if explicitOnly {
					whitelist = append(whitelist, fmt.Sprintf("-b %s", bundle.ID))
				} else {
					whitelist = append(whitelist, bundle.ID)
				}
			}
		}

		if explicitOnly {
			return strings.Join(whitelist, " "), nil
		}
		return strings.Join(whitelist, "\n"), nil
	}

	return "", fmt.Errorf("failed to find __PRELINK_INFO.__info section")
}
