package bundle

import (
	"bytes"
	"encoding/asn1"
	"encoding/binary"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"slices"
	"sort"
	"strconv"
	"strings"

	"github.com/apex/log"
	"github.com/blacktop/go-macho/types"
)

const Magic = "BUND"

type Header struct {
	Unknown1 uint16  // 0x0200
	Unknown2 uint16  // 0x1400
	_        uint32  // padding ?
	Magic    [4]byte // "BUND"
	_        uint16  // padding ?
	Type     uint16  // 3 (legacy Exclave), 4 (range-table bundle)
}

type Bundle struct {
	Header
	TypeHeader any
	Config     Config
	Files      []File

	r      io.ReadSeeker
	closer any
}

func (b Bundle) String() string {
	var s strings.Builder
	s.WriteString(fmt.Sprintf("Bundle: %s\n", string(b.Magic[:])))
	s.WriteString(fmt.Sprintf("  Type: %d\n", b.Type))
	switch b.Type {
	case 3:
		b.writeConfig(&s)
	case 4:
		s.WriteString("  Ranges:\n")
		for i, f := range b.TypeHeader.(Type4).Ranges {
			s.WriteString(fmt.Sprintf("    %3s) %s\n", fmt.Sprintf("%d", i+1), f))
		}
		if len(b.Files) > 0 {
			b.writeConfig(&s)
		}
	}
	return s.String()
}

func (b Bundle) writeConfig(s *strings.Builder) {
	s.WriteString("  Config:\n")
	s.WriteString(fmt.Sprintf("    Unk1: %d\n", b.Config.Unk1))
	s.WriteString(fmt.Sprintf("    Unk2: %d\n", b.Config.Unk2))
	s.WriteString("    Assets:\n")
	for i, h := range b.Config.Assets {
		s.WriteString(fmt.Sprintf("      %3s) %-20s\n", fmt.Sprintf("%d", i+1), h))
	}
	s.WriteString("    TOC:\n")
	for _, t := range b.Config.TOC {
		s.WriteString(fmt.Sprintf("      %s\n", t))
	}
	s.WriteString("Compartments:\n")
	for _, f := range b.Files {
		s.WriteString(fmt.Sprintf("%s\n", f))
	}
}

type Segment struct {
	Name   string
	Offset uint64
	Size   uint64
}

type Section struct {
	Name   string
	Offset uint64
	Size   uint64
}

type File struct {
	Name      string
	Type      string
	Segments  []Segment
	Sections  []Section
	Endpoints []Endpoint
	Extra     map[string]any
}

func (f File) Segment(name string) *Segment {
	for _, seg := range f.Segments {
		if seg.Name == name {
			return &seg
		}
	}
	return nil
}

func (f File) String() string {
	var s strings.Builder
	s.WriteString(fmt.Sprintf("  %s (%s)\n", f.Name, f.Type))
	for _, seg := range f.Segments {
		if seg.Size == 0 {
			continue
		}
		if seg.Name == "HEADER" {
			s.WriteString(fmt.Sprintf("    sz=0x%08x off=0x%08x-0x%08x __%s\n", seg.Size, seg.Offset, seg.Offset+seg.Size, seg.Name))
		}
	}
	for _, sec := range f.Sections {
		if sec.Size == 0 {
			continue
		}
		s.WriteString(fmt.Sprintf("      sz=0x%08x off=0x%08x-0x%08x __%s\n", sec.Size, sec.Offset, sec.Offset+sec.Size, sec.Name))
	}
	if len(f.Endpoints) > 0 {
		s.WriteString("    endpoints:\n")
		for i, ep := range f.Endpoints {
			s.WriteString(fmt.Sprintf("      %3s) %s\n", fmt.Sprintf("%d", i+1), ep))
		}
	}
	if len(f.Extra) > 0 {
		s.WriteString("    extra:\n")
		for k, v := range f.Extra {
			s.WriteString(fmt.Sprintf("      %s: %v\n", k, v))
		}
	}
	return s.String()
}

type Type3 struct {
	UUID             types.UUID
	_                [4]uint64 // padding ?
	SubType          uint64
	_                uint64 // padding ?
	FileSz           uint64
	_                uint64 // padding ?
	EndOff           uint64
	DataOff          uint64 // roottask __DATA offset
	_                uint64 // padding ?
	KernelDataOffset uint64 // 2D0C000h
	_                uint64 // padding ?
	TextOff          uint64 // roottask __TEXT offset
	UnkSz            uint64
	TextOffAgain     uint64 // ???
	TextSz           uint64 // is x14000 again
	UnkSzMaybe       uint64 // 2CF8000h
	_                uint64 // padding ?
	UnkOffMaybe      uint64 // 5FC000h
	UnkNumOfImgs     uint64 // C0000000h ? maybe just 0xC == 11 ?
	What             uint64 // C0035404h
	UnkSz2           uint64 // 5FC000h
	Something        uint64 // 26FC000h
	KernelDataOff    uint64 // 2D0C000h
	SomeSize2        uint64 // 334000h
	_                uint64 // padding ?
	AnotherSz        uint64 // 30000h ?
	_                uint64 // padding ?
	_                uint64 // padding ?
	YouAgain         uint64 // 30000h ?
	UnkSize4         uint64 // 74000h ?
	UnkSize5         uint64 // 40000h ?
	HelpMe           uint64 // C05FC000h ?
	UnkSize6         uint64 // 264000h ?
	Nooooooo         uint64 // 14000h ???
	SweetBaaaby      uint64 // C0670000h ??
	ImDead           uint64 // 70000h ??
	_                uint64 // padding ?
	Aaaagain         uint64 // 70000h ??
	UnkOffset6       uint64 // 2BC000h ???
	UnkOffset7       uint64 // 2D0C000h ???
	UnkOffset8       uint64 // 334000h ???
	Aaaaaaaaaagin    uint64 // 70000h ???
	_                uint64 // padding ?
	Aaaaaaaaaagin2   uint64 // 70000h ???
	UnkOffset9       uint64 // 2BC000h ???
	UnkOffset10      uint64 // 32C000h ???
	FooterOffset     uint64
	FooterSz         uint64
}

type Type4 struct {
	Unk0       uint32         // 1
	Unk1       uint32         // 0xc == 11
	_          uint64         // padding ?
	Unk2       uint64         // F000h
	_          [4]uint64      // padding ?
	Unk3       uint64         // C000h
	_          [2]uint64      // padding ?
	Unk4       uint64         // 3 ?
	_          uint64         // padding ?
	Unk5       uint64         // 16000h ?
	_          uint64         // padding ?
	_          uint64         // padding ?
	NumRanges  uint64         // 0xD == 12 ?
	UUID       types.UUID     // 636B62C3-4647-34F7-9089-A58256078A27
	_          uint64         // padding ?
	Unk7       uint64         // 14000h ?
	Unk7again  uint64         // 14000h ?
	Unk8       uint64         // 8000000h ?
	Unk9       uint64         // 1 ?
	_          uint64         // padding ?
	Unk10      uint64         // C000h ?
	Unk10again uint64         // C000h ?
	Unk11      uint64         // 8014000h ?
	Unk12      uint64         // 4 ?
	_          [3]uint64      // padding ?
	Unk13      uint64         // 8020000h ?
	Unk14      uint64         // 6 ?
	Unk15      uint64         // 8003E80h ?
	_          [32]uint64     // padding ?
	ConfigOff  uint64         // offset from urst range to ASN.1 config
	ConfigPad  uint64         // padded config allocation size
	ConfigSz   uint64         // ASN.1 config size
	_          uint64         // padding ?
	Unk17      uint64         // 0xa == 10 ?
	ConfigOff2 uint64         // offset from urst range to ASN.1 config
	Unk18      uint64         // 16000h ?
	Unk19      uint64         // 1582Ch ?
	_          uint64         // padding ?
	Unk20      uint64         // 0xa == 10 ?
	_          uint64         // padding ?
	Unk21      uint64         // F000h ?
	Ranges     [0xD]typ4Range // type 4 file table ranges
}

type typ4Range struct {
	Type   uint32
	Name   [4]byte
	Offset uint64
	Size   uint64
}

func (t4 typ4Range) String() string {
	name := t4.NameString()
	if t4.Size == 0 {
		return fmt.Sprintf("typ=%d sz=%-10s off=0x%08x-0x%08x %s", t4.Type, fmt.Sprintf("%d", t4.Size), t4.Offset, t4.Offset+t4.Size, name)
	}
	return fmt.Sprintf("typ=%d sz=0x%08x off=0x%08x-0x%08x %s", t4.Type, t4.Size, t4.Offset, t4.Offset+t4.Size, name)
}

func (t4 typ4Range) NameString() string {
	name := t4.Name
	slices.Reverse(name[:])
	return strings.TrimRight(string(name[:]), "\x00")
}

func (t4 Type4) rangeByName(name string) (typ4Range, bool) {
	for _, rng := range t4.Ranges {
		if rng.NameString() == name {
			return rng, true
		}
	}
	return typ4Range{}, false
}

type Config struct {
	Unk1         int
	Unk2         int
	Assets       []Asset
	TOC          []TocEntry
	Compartments []Compartment
}

type Asset struct {
	Raw    asn1.RawContent
	Name   asn1.RawValue
	Type   int
	Offset int
	Size   int
}

func (h Asset) String() string {
	return fmt.Sprintf("%15s type=%d off=%#07x sz=%#x", h.Name.Bytes, h.Type, h.Offset, h.Size)
}

type TocEntry struct {
	Index int
	Entry asn1.RawValue `asn1:"optional"`
}

type TocEntryType struct {
	Name asn1.RawValue
	Type int
}

func (t TocEntry) GetEntry() *TocEntryType {
	if len(t.Entry.Bytes) > 0 {
		var typ TocEntryType
		if _, err := asn1.Unmarshal(t.Entry.Bytes, &typ); err == nil {
			return &typ
		}
	}
	return nil
}

func (t TocEntry) String() string {
	if entry := t.GetEntry(); entry != nil {
		return fmt.Sprintf("%3d) %15s type=%d", t.Index, entry.Name.Bytes, entry.Type)
	}
	return fmt.Sprintf("%3d) %s", t.Index, "nil")
}

type Compartment struct {
	Raw      asn1.RawContent
	AppUID   int
	Metadata []metadata
}

type metadata struct {
	Raw   asn1.RawContent
	Key   asn1.RawValue
	Value asn1.RawValue
}

type Endpoint struct {
	Type int
	Unk1 asn1.RawValue
	Unk2 asn1.RawValue
	Name asn1.RawValue
}

func (e Endpoint) String() string {
	return string(e.Name.Bytes)
}

func (md metadata) ParseValue() (any, error) {
	if bytes.HasPrefix(md.Key.Bytes, []byte("__COMPONENT")) {
		return string(md.Value.Bytes), nil
	}
	if bytes.HasPrefix(md.Key.Bytes, []byte("__ENDPOINT")) {
		var e Endpoint
		if _, err := asn1.Unmarshal(md.Value.Bytes, &e); err == nil {
			return e, nil
		} else {
			return nil, fmt.Errorf("failed to unmarshal bundle file info value: %v", err)
		}
	}
	if len(md.Value.Bytes) <= 8 {
		var num uint64
		for idx, b := range md.Value.Bytes {
			num |= uint64(b) << (8 * uint64(len(md.Value.Bytes)-1-idx))
		}
		return num, nil
	}
	return md.Value.Bytes, nil
}

func (md metadata) String() string {
	val, err := md.ParseValue()
	if err != nil {
		return fmt.Sprintf("[ERROR] failed to parse value: %v", err)
	}
	switch v := val.(type) {
	case string:
		return fmt.Sprintf("%s: %s", string(md.Key.Bytes), v)
	case Endpoint:
		return fmt.Sprintf("%s: %s", string(md.Key.Bytes), v)
	case uint64:
		if len(md.Value.Bytes) == 1 {
			return fmt.Sprintf("%s: %d", string(md.Key.Bytes), v)
		}
		return fmt.Sprintf("%s: %#x", string(md.Key.Bytes), v)
	default:
		return fmt.Sprintf("%s: %v", string(md.Key.Bytes), v)
	}
}

func (b *Bundle) ParseFiles() error {
	for _, bf := range b.Config.Compartments {
		var f File
		var sec Section
		var seg Segment
		entpoints := make(map[int]Endpoint, 0)
		f.Extra = make(map[string]any)
		for _, md := range bf.Metadata {
			val, err := md.ParseValue()
			if err != nil {
				return fmt.Errorf("failed to parse bundle file info value: %v", err)
			}
			if strings.EqualFold(string(md.Key.Bytes), "__COMPONENTNAME") {
				f.Name = val.(string)
			} else if strings.EqualFold(string(md.Key.Bytes), "__COMPONENTTYPE") {
				f.Type = val.(string)
			} else if _, secpart, ok := strings.Cut(string(md.Key.Bytes), "__MACHO__"); ok { // SECTION
				if name, _, ok := strings.Cut(secpart, "OFF"); ok {
					sec.Name = name
					sec.Offset = val.(uint64)
				}
				if name, _, ok := strings.Cut(secpart, "SZ"); ok {
					if sec.Name == "" {
						sec.Name = name
					}
					sec.Size = val.(uint64)
				}
				if sec.Name != "" && sec.Size != 0 {
					f.Sections = append(f.Sections, sec)
					sec = Section{}
				}
			} else if _, segpart, ok := strings.Cut(string(md.Key.Bytes), "__MACHO"); ok { // SEGMENT
				if name, _, ok := strings.Cut(segpart, "OFF"); ok {
					seg.Name = name
					seg.Offset = val.(uint64)
				}
				if name, _, ok := strings.Cut(segpart, "SZ"); ok {
					if seg.Name == "" {
						seg.Name = name
					}
					seg.Size = val.(uint64)
				}
				if seg.Name != "" && seg.Size != 0 {
					f.Segments = append(f.Segments, seg)
					seg = Segment{}
				}
			} else if _, idx, ok := strings.Cut(string(md.Key.Bytes), "__ENDPOINT__"); ok { // ENDPOINT
				if i, err := strconv.Atoi(idx); err == nil {
					entpoints[i] = val.(Endpoint)
				} else {
					return fmt.Errorf("failed to parse bundle file endpoint index: %v", err)
				}
			} else {
				f.Extra[string(md.Key.Bytes)] = string(md.Value.Bytes)
			}
		}
		sort.Slice(f.Sections, func(i, j int) bool {
			return f.Sections[i].Offset < f.Sections[j].Offset
		})
		sort.Slice(f.Segments, func(i, j int) bool {
			return f.Segments[i].Offset < f.Segments[j].Offset
		})
		keys := make([]int, 0, len(entpoints))
		for k := range entpoints {
			keys = append(keys, k)
		}
		sort.Ints(keys)
		for _, k := range keys {
			f.Endpoints = append(f.Endpoints, entpoints[k])
		}
		b.Files = append(b.Files, f)
	}
	return nil
}

func (b *Bundle) DumpFiles(output string) error {
	if b.Type == 4 {
		for _, rng := range b.TypeHeader.(Type4).Ranges {
			if rng.Size > 0 {
				if _, err := b.r.Seek(int64(rng.Offset), io.SeekStart); err != nil {
					return fmt.Errorf("failed to seek to bundle config data: %v", err)
				}
				data := make([]byte, rng.Size)
				if _, err := b.r.Read(data); err != nil {
					return fmt.Errorf("failed to read bundle data: %v", err)
				}
				fname := filepath.Join(output, rng.NameString()+".bin")
				log.WithField("name", fname).Info("Creating")
				if err := os.WriteFile(fname, data, 0o644); err != nil {
					return fmt.Errorf("failed to write bundle data to file: %v", err)
				}
			}
		}
	} else {
		return fmt.Errorf("unsupported bundle type: %d", b.Type)
	}
	return nil
}

func (b *Bundle) parseType4Config(t4 Type4) error {
	if t4.ConfigSz == 0 {
		return nil
	}

	urst, ok := t4.rangeByName("urst")
	if !ok {
		return nil
	}
	if t4.ConfigOff > urst.Size || t4.ConfigSz > urst.Size-t4.ConfigOff {
		return fmt.Errorf("type 4 bundle config range out of bounds: urst=0x%x config_off=0x%x config_sz=0x%x", urst.Size, t4.ConfigOff, t4.ConfigSz)
	}
	if t4.ConfigPad != 0 && t4.ConfigSz > t4.ConfigPad {
		return fmt.Errorf("type 4 bundle config size exceeds padded allocation: config_sz=0x%x config_pad=0x%x", t4.ConfigSz, t4.ConfigPad)
	}

	configOffset := urst.Offset + t4.ConfigOff
	if _, err := b.r.Seek(int64(configOffset), io.SeekStart); err != nil {
		return fmt.Errorf("failed to seek to type 4 bundle config data: %v", err)
	}

	fdata := make([]byte, t4.ConfigSz)
	if _, err := io.ReadFull(b.r, fdata); err != nil {
		return fmt.Errorf("failed to read type 4 bundle config data: %v", err)
	}
	if fdata[0] != 0x30 {
		return nil
	}

	if _, err := asn1.Unmarshal(fdata, &b.Config); err != nil {
		return fmt.Errorf("failed to ASN.1 parse type 4 bundle config: %v", err)
	}

	if err := b.ParseFiles(); err != nil {
		return fmt.Errorf("failed to parse type 4 bundle files: %v", err)
	}

	return nil
}

func Open(filename string) (*Bundle, error) {
	f, err := os.Open(filename)
	if err != nil {
		return nil, fmt.Errorf("failed to open file: %v", err)
	}
	bundle, err := Parse(f)
	if err != nil {
		f.Close()
		return nil, err
	}
	bundle.closer = f
	return bundle, nil
}

func (b *Bundle) Close() error {
	if b.closer != nil {
		err := b.closer.(io.Closer).Close()
		b.closer = nil
		return err
	}
	return nil
}

func Parse(r io.ReadSeeker) (*Bundle, error) {
	var bn Bundle
	bn.r = r

	if err := binary.Read(r, binary.LittleEndian, &bn.Header); err != nil {
		return nil, fmt.Errorf("failed to read bundle header: %v", err)
	}
	slices.Reverse(bn.Magic[:])

	if string(bn.Magic[:]) != Magic {
		return nil, fmt.Errorf("invalid magic: %s; expected 'BUND'", string(bn.Magic[:]))
	}

	switch bn.Type {
	case 3: // legacy ASN.1-footer Exclave bundle
		var t3 Type3
		if err := binary.Read(r, binary.LittleEndian, &t3); err != nil {
			return nil, fmt.Errorf("failed to read bundle type 3: %v", err)
		}
		bn.TypeHeader = t3
		// parse footer/config
		if _, err := r.Seek(-int64(t3.FooterOffset), io.SeekEnd); err != nil {
			return nil, fmt.Errorf("failed to seek to bundle config data: %v", err)
		}

		fdata := make([]byte, t3.FooterSz)
		if _, err := r.Read(fdata); err != nil {
			return nil, fmt.Errorf("failed to read bundle data: %v", err)
		}

		if _, err := asn1.Unmarshal(fdata, &bn.Config); err != nil {
			return nil, fmt.Errorf("failed to ASN.1 parse bundle config: %v", err)
		}

		if err := bn.ParseFiles(); err != nil {
			return nil, fmt.Errorf("failed to parse bundle files: %v", err)
		}
	case 4: // range-table bundle, including the new ExclaveCore layout
		var t4 Type4
		if err := binary.Read(r, binary.LittleEndian, &t4); err != nil {
			return nil, fmt.Errorf("failed to read bundle type 4: %v", err)
		}
		bn.TypeHeader = t4
		if err := bn.parseType4Config(t4); err != nil {
			return nil, err
		}
		for _, rng := range t4.Ranges {
			switch rng.NameString() {
			// case "uedt":
			// 	// parse device tree/config
			// 	log.WithField("name", rng.NameString()).Debug("Device Tree")
			// 	if _, err := r.Seek(int64(rng.Offset), io.SeekStart); err != nil {
			// 		return nil, fmt.Errorf("failed to seek to bundle config data: %v", err)
			// 	}
			// 	dtdata := make([]byte, rng.Size)
			// 	if _, err := r.Read(dtdata); err != nil {
			// 		return nil, fmt.Errorf("failed to read bundle data: %v", err)
			// 	}
			// 	dt, err := devicetree.ParseData(bytes.NewReader(dtdata))
			// 	if err != nil {
			// 		os.WriteFile("uedt.bin", dtdata, 0o644)
			// 		return nil, fmt.Errorf("failed to parse device tree: %v", err)
			// 	}
			// 	log.Debug(dt.String())
			default:
				log.WithField("name", rng.NameString()).Debug("unsupported")
			}
		}
	default:
		return nil, fmt.Errorf("unknown bundle type: %d", bn.Type)
	}

	return &bn, nil
}
