package iokit

import (
	"fmt"
	"io"
	"maps"
	"path/filepath"
	"regexp"
	"sort"
	"strings"

	"github.com/blacktop/go-macho"
	mtypes "github.com/blacktop/go-macho/types"
	"github.com/blacktop/ipsw/pkg/kernelcache"
	"github.com/blacktop/ipsw/pkg/kernelcache/cpp"
	"github.com/blacktop/ipsw/pkg/signature"
	"github.com/blacktop/ipsw/pkg/symbols"
)

const (
	defaultMaxFunctionInstructions       = 320
	defaultSwitchMaxFunctionInstructions = 2048
	// defaultMaxVtableSlots is the upper bound on vtable slots inspected when
	// locating externalMethod. On recent kernelcaches the IOUserClient and
	// IOUserClient2022 vtables place externalMethod at slot 272 and the legacy
	// getTargetAndMethodForIndex at slot 302, so the bound must clear those
	// indices. VtableEntries terminates at the genuine vtable end on its own
	// (the first non-pointer slot), so this is only a safety ceiling.
	defaultMaxVtableSlots = 768
)

type analyzer struct {
	root          *macho.File
	scanner       *cpp.Scanner
	classes       []cpp.Class
	infos         []*classInfo
	byName        map[string][]*classInfo
	symbolMap     map[uint64]string
	symCache      map[uint64]string
	methods       map[methodCacheKey]methodAnalysis
	ucRefs        map[uint64][]string
	stderr        io.Writer
	maxInst       int
	maxSwitchInst int
	maxSlots      int
	userClient    map[string]struct{}
	namedByVtable map[uint64][]cpp.VtableEntry
}

type classInfo struct {
	cpp.Class
	index      int
	family     string
	isService  bool
	userClient bool
}

func Scan(root *macho.File, conf Config) ([]Record, error) {
	if root == nil {
		return nil, fmt.Errorf("nil kernelcache")
	}
	maxInst, maxSwitchInst := normalizeMaxInstructionCaps(conf.MaxFunctionInstructions)
	if conf.MaxVtableSlots == 0 {
		conf.MaxVtableSlots = defaultMaxVtableSlots
	}
	classFilter, err := compileClassFilter(conf.ClassFilter)
	if err != nil {
		return nil, err
	}

	scanner := cpp.NewScanner(root, cpp.Config{})
	classes, err := scanner.Scan()
	if err != nil {
		return nil, fmt.Errorf("cpp scan: %w", err)
	}

	a := newAnalyzer(root, scanner, classes, conf, maxInst, maxSwitchInst)
	userClients := a.userClientClasses()
	if len(userClients) == 0 {
		return nil, ErrNoIOUserClients
	}
	progress(conf.Stderr, "kernel iokit-methods: discovered %d IOUserClient subclasses\n", len(userClients))

	records, err := a.methodRecords(userClients)
	if err != nil {
		return nil, err
	}
	records = append(records, a.serviceClientRecords()...)
	records = dedupeRecords(records)
	records = filterRecordsByClass(records, classFilter)
	SortRecords(records)
	return records, nil
}

func normalizeMaxInstructionCaps(requestedMaxInst int) (int, int) {
	if requestedMaxInst <= 0 {
		return defaultMaxFunctionInstructions, defaultSwitchMaxFunctionInstructions
	}
	return requestedMaxInst, requestedMaxInst
}

func newAnalyzer(root *macho.File, scanner *cpp.Scanner, classes []cpp.Class, conf Config, maxInst, maxSwitchInst int) *analyzer {
	a := &analyzer{
		root:          root,
		scanner:       scanner,
		classes:       classes,
		byName:        make(map[string][]*classInfo),
		symbolMap:     loadKernelSymbolMap(conf.Kernelcache, conf.Stderr),
		symCache:      make(map[uint64]string),
		methods:       make(map[methodCacheKey]methodAnalysis),
		ucRefs:        make(map[uint64][]string),
		stderr:        conf.Stderr,
		maxInst:       maxInst,
		maxSwitchInst: maxSwitchInst,
		maxSlots:      conf.MaxVtableSlots,
		userClient:    make(map[string]struct{}),
	}
	a.buildClassInfos()
	return a
}

func (a *analyzer) buildClassInfos() {
	a.infos = make([]*classInfo, 0, len(a.classes))
	for idx := range a.classes {
		info := &classInfo{
			Class: a.classes[idx],
			index: idx,
		}
		if reachesClass(a.classes, idx, "IOUserClient2022") {
			info.family = "IOUserClient2022"
		} else if reachesClass(a.classes, idx, "IOUserClient") {
			info.family = "IOUserClient"
		}
		info.userClient = info.family != "" &&
			info.Name != "IOUserClient" &&
			info.Name != "IOUserClient2022"
		info.isService = reachesClass(a.classes, idx, "IOService") && info.Name != "IOService"
		a.infos = append(a.infos, info)
		a.byName[info.Name] = append(a.byName[info.Name], info)
		if info.userClient {
			a.userClient[info.Name] = struct{}{}
		}
	}
	for _, infos := range a.byName {
		sort.SliceStable(infos, func(i, j int) bool {
			if infos[i].Bundle != infos[j].Bundle {
				return infos[i].Bundle < infos[j].Bundle
			}
			return infos[i].MetaPtr < infos[j].MetaPtr
		})
	}
}

func reachesClass(classes []cpp.Class, idx int, target string) bool {
	seen := make(map[int]struct{})
	for idx >= 0 && idx < len(classes) {
		if _, ok := seen[idx]; ok {
			return false
		}
		seen[idx] = struct{}{}
		if classes[idx].Name == target {
			return true
		}
		idx = classes[idx].SuperIndex
	}
	return false
}

func (a *analyzer) userClientClasses() []*classInfo {
	out := make([]*classInfo, 0, 128)
	for _, info := range a.infos {
		if info.userClient {
			out = append(out, info)
		}
	}
	sort.SliceStable(out, func(i, j int) bool {
		if out[i].Bundle != out[j].Bundle {
			return out[i].Bundle < out[j].Bundle
		}
		return out[i].Name < out[j].Name
	})
	return out
}

func (a *analyzer) methodRecords(userClients []*classInfo) ([]Record, error) {
	slots, err := a.externalMethodSlots(userClients)
	if err != nil {
		return nil, err
	}
	legacySlots := a.legacyMethodSlots(userClients)
	records := make([]Record, 0, len(userClients))
	for _, info := range userClients {
		slot, ok := slots[info.family]
		if !ok {
			records = append(records, a.unknownMethodRecord(info, 0, "vtable_unresolved"))
			continue
		}
		entry, ok := a.vtableEntry(info.Class, slot)
		if !ok || entry.Address == 0 {
			records = append(records, a.unknownMethodRecord(info, 0, "vtable_unresolved"))
			continue
		}
		analysis := a.analyzeExternalMethod(entry.Address, dispatchKindHintForFamily(info.family))
		switch analysis.kind {
		case DispatchExternalMethod, DispatchExternalMethod2022:
			recs, err := a.dispatchRecords(info, analysis)
			if err != nil {
				return nil, err
			}
			annotateExternalMethodVtableRecords(recs, entry)
			records = append(records, recs...)
		case DispatchSwitch:
			recs := a.switchRecords(info, analysis)
			annotateExternalMethodVtableRecords(recs, entry)
			records = append(records, recs...)
		default:
			if recs, ok, err := a.legacyMethodRecordsForInfo(info, legacySlots); err != nil {
				return nil, err
			} else if ok {
				records = append(records, recs...)
				continue
			}
			if strings.Contains(a.symbolName(entry.Address), ioavExternalMethodSymbol) {
				if ioavAnalysis, ok := a.resolveIOAVDispatch(info); ok {
					recs, err := a.dispatchRecords(info, ioavAnalysis)
					if err != nil {
						return nil, err
					}
					annotateExternalMethodVtableRecords(recs, entry)
					records = append(records, recs...)
					continue
				}
			}
			note := analysis.note
			if note == "" {
				note = "indirect"
			}
			rec := a.unknownMethodRecord(info, entry.Address, note)
			annotateExternalMethodVtable(&rec, entry)
			records = append(records, rec)
		}
	}
	return records, nil
}

func (a *analyzer) externalMethodSlots(userClients []*classInfo) (map[string]int, error) {
	symbolSlots := make(map[string]map[int]int)
	for _, info := range userClients {
		for _, entry := range a.scanner.VtableEntries(info.Class, a.maxSlots) {
			if isNamedMethod(a.symbolName(entry.Address), "externalMethod") {
				addSlot(symbolSlots, info.family, entry.Index)
			}
		}
	}

	out := make(map[string]int)
	for family, slots := range symbolSlots {
		slot, err := chooseSlot(family, slots, "symbol")
		if err != nil {
			return nil, err
		}
		out[family] = slot
	}

	patternSlots := make(map[string]map[int]int)
	for _, info := range userClients {
		if _, ok := out[info.family]; ok {
			continue
		}
		for _, entry := range a.scanner.VtableEntries(info.Class, a.maxSlots) {
			analysis := a.analyzeExternalMethod(entry.Address, dispatchKindHintForFamily(info.family))
			if analysis.kind == DispatchExternalMethod ||
				analysis.kind == DispatchExternalMethod2022 ||
				analysis.kind == DispatchSwitch {
				addSlot(patternSlots, info.family, entry.Index)
			}
		}
	}
	for family, slots := range patternSlots {
		if _, ok := out[family]; ok {
			continue
		}
		slot, err := chooseSlot(family, slots, "pattern")
		if err != nil {
			return nil, err
		}
		out[family] = slot
	}
	return out, nil
}

func (a *analyzer) legacyMethodSlots(userClients []*classInfo) map[string]int {
	symbolSlots := make(map[string]map[int]int)
	for _, info := range userClients {
		for _, entry := range a.scanner.VtableEntries(info.Class, a.maxSlots) {
			if isNamedMethod(a.symbolName(entry.Address), "getTargetAndMethodForIndex") {
				addSlot(symbolSlots, info.family, entry.Index)
			}
		}
	}
	out := make(map[string]int)
	for family, slots := range symbolSlots {
		if slot, err := chooseSlot(family, slots, "legacy"); err == nil {
			out[family] = slot
		}
	}

	patternSlots := make(map[string]map[int]int)
	for _, info := range userClients {
		if _, ok := out[info.family]; ok {
			continue
		}
		for _, entry := range a.scanner.VtableEntries(info.Class, a.maxSlots) {
			analysis := a.analyzeLegacyExternalMethod(entry.Address)
			if analysis.kind == DispatchExternalMethodLegacy {
				addSlot(patternSlots, info.family, entry.Index)
			}
		}
	}
	for family, slots := range patternSlots {
		if _, ok := out[family]; ok {
			continue
		}
		if slot, err := chooseSlot(family, slots, "legacy pattern"); err == nil {
			out[family] = slot
		}
	}
	return out
}

func (a *analyzer) legacyMethodRecordsForInfo(info *classInfo, legacySlots map[string]int) ([]Record, bool, error) {
	slot, ok := legacySlots[info.family]
	if !ok {
		return nil, false, nil
	}
	entry, ok := a.vtableEntry(info.Class, slot)
	if !ok || entry.Address == 0 {
		return nil, false, nil
	}
	analysis := a.analyzeLegacyExternalMethod(entry.Address)
	if analysis.kind != DispatchExternalMethodLegacy {
		return nil, false, nil
	}
	records, err := a.legacyDispatchRecords(info, analysis)
	if err != nil {
		return nil, false, err
	}
	annotateExternalMethodVtableRecords(records, entry)
	return records, true, nil
}

func addSlot(slots map[string]map[int]int, family string, slot int) {
	if family == "" || slot < 0 {
		return
	}
	if slots[family] == nil {
		slots[family] = make(map[int]int)
	}
	slots[family][slot]++
}

func chooseSlot(family string, slots map[int]int, source string) (int, error) {
	if len(slots) == 0 {
		return -1, fmt.Errorf("no %s externalMethod slot candidates for %s", source, family)
	}
	type candidate struct {
		slot  int
		count int
	}
	candidates := make([]candidate, 0, len(slots))
	for slot, count := range slots {
		candidates = append(candidates, candidate{slot: slot, count: count})
	}
	sort.Slice(candidates, func(i, j int) bool {
		if candidates[i].count != candidates[j].count {
			return candidates[i].count > candidates[j].count
		}
		return candidates[i].slot < candidates[j].slot
	})
	if len(candidates) > 1 && candidates[0].count == candidates[1].count {
		return -1, fmt.Errorf("externalMethod vtable slot differs across %s hierarchy (%s candidates: %v)", family, source, slots)
	}
	return candidates[0].slot, nil
}

func (a *analyzer) vtableEntry(class cpp.Class, slot int) (cpp.VtableEntry, bool) {
	if a == nil || a.scanner == nil {
		return cpp.VtableEntry{}, false
	}
	// Prefer the named method table: its entries carry the same PAC metadata as
	// VtableSlotPAC plus the resolved Class/Method/Mangled names, so external
	// method records are fully annotated. Fall back to the raw slot accessors for
	// slots past the recovered method count.
	if methods := a.namedMethods(class); slot >= 0 && slot < len(methods) {
		return methods[slot], true
	}
	if entry, ok := a.scanner.VtableSlotPAC(class, slot); ok {
		return entry, true
	}
	return a.scanner.VtableEntry(class, slot)
}

// namedMethods returns the named vtable method entries for class, or nil. The
// per-class tables are built once from the full discovered class set (naming
// needs the whole hierarchy to resolve overrides and inherited names) and cached
// by vtable address.
func (a *analyzer) namedMethods(class cpp.Class) []cpp.VtableEntry {
	if a == nil || a.scanner == nil || class.VtableAddr == 0 {
		return nil
	}
	if a.namedByVtable == nil {
		tables := a.scanner.BuildNamedMethodTables(a.classes)
		a.namedByVtable = make(map[uint64][]cpp.VtableEntry, len(tables))
		for _, mt := range tables {
			if mt.VtableAddr != 0 {
				a.namedByVtable[mt.VtableAddr] = mt.Methods
			}
		}
	}
	return a.namedByVtable[class.VtableAddr]
}

func isNamedMethod(symbolName string, method string) bool {
	if symbolName == "" {
		return false
	}
	return strings.Contains(symbolName, "::"+method+"(") ||
		strings.Contains(symbolName, "::"+method+" ")
}

func (a *analyzer) unknownMethodRecord(info *classInfo, methodAddr uint64, note string) Record {
	extra := map[string]string{"slice_notes": note}
	return Record{
		Kind:              KindMethod,
		Class:             info.Name,
		Bundle:            info.Bundle,
		Selector:          -1,
		MethodAddr:        hexAddr(methodAddr),
		MethodSymbol:      a.symbolName(methodAddr),
		DispatchKind:      DispatchUnknown,
		ScalarInputCount:  -1,
		ScalarOutputCount: -1,
		StructInputSize:   -1,
		StructOutputSize:  -1,
		Flags:             -1,
		Resolved:          false,
		Extra:             extra,
	}
}

func (a *analyzer) symbolName(addr uint64) string {
	if a == nil || addr == 0 {
		return ""
	}
	if a.symCache != nil {
		if cached, ok := a.symCache[addr]; ok {
			return cached
		}
	}
	if name := a.symbolMap[addr]; name != "" {
		return a.cacheSymbolName(addr, symbols.DemangleSymbolName(name))
	}
	if a.scanner == nil {
		return ""
	}
	if name := a.scanner.SymbolName(addr); name != "" {
		return a.cacheSymbolName(addr, name)
	}
	if body, err := a.scanner.FunctionBodyAt(addr); err == nil && body.Function.StartAddr != addr {
		if name := a.symbolMap[body.Function.StartAddr]; name != "" {
			return a.cacheSymbolName(addr, symbols.DemangleSymbolName(name))
		}
		if name := a.scanner.SymbolName(body.Function.StartAddr); name != "" {
			return a.cacheSymbolName(addr, name)
		}
	}
	return a.cacheSymbolName(addr, "")
}

func (a *analyzer) cacheSymbolName(addr uint64, name string) string {
	if a != nil && a.symCache != nil {
		a.symCache[addr] = name
	}
	return name
}

func loadKernelSymbolMap(path string, stderr io.Writer) map[uint64]string {
	out := make(map[uint64]string)
	if path == "" {
		return out
	}
	smap := signature.NewSymbolMap()
	for _, candidate := range kernelSymbolMapCandidates(path) {
		if err := smap.LoadJSON(candidate); err == nil {
			maps.Copy(out, smap)
			progress(stderr, "kernel iokit-methods: loaded symbol map %s\n", candidate)
			return out
		}
	}
	return out
}

func kernelSymbolMapCandidates(path string) []string {
	base := filepath.Base(path) + ".symbols.json"
	seen := make(map[string]struct{})
	var out []string
	add := func(candidate string) {
		candidate = filepath.Clean(candidate)
		if _, ok := seen[candidate]; ok {
			return
		}
		seen[candidate] = struct{}{}
		out = append(out, candidate)
	}
	dir := filepath.Dir(path)
	for range 4 {
		add(filepath.Join(dir, base))
		matches, _ := filepath.Glob(filepath.Join(dir, "*", base))
		sort.Strings(matches)
		for _, match := range matches {
			add(match)
		}
		next := filepath.Dir(dir)
		if next == dir {
			break
		}
		dir = next
	}
	return out
}

func (a *analyzer) serviceClientRecords() []Record {
	records := a.personalityRecords()
	records = append(records, a.newUserClientRecords()...)
	return records
}

func classBundle(infos []*classInfo) string {
	if len(infos) == 0 {
		return ""
	}
	return infos[0].Bundle
}

func classExists(infos []*classInfo) bool {
	return len(infos) > 0
}

func hexAddr(addr uint64) string {
	if addr == 0 {
		return "0x0"
	}
	return fmt.Sprintf("%#x", addr)
}

func dedupeRecords(records []Record) []Record {
	if len(records) < 2 {
		return records
	}
	SortRecords(records)
	out := records[:0]
	var last string
	for _, rec := range records {
		key := recordKey(rec)
		if key == last {
			continue
		}
		out = append(out, rec)
		last = key
	}
	return out
}

func compileClassFilter(pattern string) (*regexp.Regexp, error) {
	pattern = strings.TrimSpace(pattern)
	if pattern == "" {
		return nil, nil
	}
	re, err := regexp.Compile(pattern)
	if err != nil {
		return nil, fmt.Errorf("invalid class filter %q: %w", pattern, err)
	}
	return re, nil
}

func filterRecordsByClass(records []Record, classFilter *regexp.Regexp) []Record {
	if classFilter == nil || len(records) == 0 {
		return records
	}
	out := records[:0]
	for _, rec := range records {
		if recordMatchesClassFilter(rec, classFilter) {
			out = append(out, rec)
		}
	}
	return out
}

func recordMatchesClassFilter(rec Record, classFilter *regexp.Regexp) bool {
	if classFilter == nil {
		return true
	}
	return classFilter.MatchString(rec.Class) ||
		classFilter.MatchString(rec.ServiceClass) ||
		classFilter.MatchString(rec.UserClientClass)
}

func recordKey(r Record) string {
	return strings.Join([]string{
		r.Kind,
		r.Bundle,
		r.Class,
		fmt.Sprint(r.Selector),
		r.MethodAddr,
		r.DispatchKind,
		r.ServiceBundle,
		r.ServiceClass,
		r.UserClientBundle,
		r.UserClientClass,
		r.Source,
		extraSortString(r.Extra),
	}, "\x00")
}

func progress(w io.Writer, format string, args ...any) {
	if w != nil {
		fmt.Fprintf(w, format, args...)
	}
}

func rootForPrelink(root *macho.File) *macho.File {
	if root == nil || root.FileHeader.Type != mtypes.MH_FILESET {
		return root
	}
	if kernelFile, err := root.GetFileSetFileByName("com.apple.kernel"); err == nil {
		return kernelFile
	}
	if kernelFile, err := root.GetFileSetFileByName("kernel"); err == nil {
		return kernelFile
	}
	return root
}

func kextBundles(root *macho.File) []kernelcache.CFBundle {
	if root == nil {
		return nil
	}
	if bundles, err := kernelcache.GetKexts(root); err == nil {
		return bundles
	}
	if kernelRoot := rootForPrelink(root); kernelRoot != root {
		if bundles, err := kernelcache.GetKexts(kernelRoot); err == nil {
			return bundles
		}
	}
	return nil
}
