package sevenzip

import (
	"bufio"
	"bytes"
	"encoding/binary"
	"errors"
	"fmt"
	"io"
	"math"
	"math/bits"
	"time"

	"github.com/bodgit/sevenzip/internal/util"
	"github.com/bodgit/windows"
	"golang.org/x/text/encoding/unicode"
	"golang.org/x/text/transform"
)

const (
	idEnd = iota
	idHeader
	idArchiveProperties
	idAdditionalStreamsInfo
	idMainStreamsInfo
	idFilesInfo
	idPackInfo
	idUnpackInfo
	idSubStreamsInfo
	idSize
	idCRC
	idFolder
	idCodersUnpackSize
	idNumUnpackStream
	idEmptyStream
	idEmptyFile
	idAnti
	idName
	idCTime
	idATime
	idMTime
	idWinAttributes
	idComment
	idEncodedHeader
	idStartPos
	idDummy
)

var (
	errIncompleteRead         = errors.New("sevenzip: incomplete read")
	errUnexpectedID           = errors.New("sevenzip: unexpected id")
	errMissingUnpackInfo      = errors.New("sevenzip: missing unpack info")
	errWrongNumberOfFilenames = errors.New("sevenzip: wrong number of filenames")
	errUint64NonZero          = errors.New("sevenzip: uint64 value must be non-zero")
	errUint64TooLarge         = errors.New("sevenzip: uint64 value too large")
	errInvalidBindPair        = errors.New("sevenzip: invalid bind pair")
)

func checkUint64(v uint64, nonZero bool) error {
	if nonZero && v == 0 {
		return errUint64NonZero
	}

	// Ensure the value does not exceed math.MaxInt to prevent implicit
	// conversion panics when passed to make() on 32-bit architectures,
	// and limit it to math.MaxUint32 to prevent slice allocation panics
	// on 64-bit architectures.
	if v > uint64(math.MaxInt) || v > math.MaxUint32 {
		return errUint64TooLarge
	}

	return nil
}

func readUint64(r io.ByteReader) (uint64, error) {
	b, err := r.ReadByte()
	if err != nil {
		return 0, fmt.Errorf("readUint64: ReadByte error: %w", err)
	}

	l := bits.LeadingZeros8(^b)

	var v uint64
	if l < 7 {
		v |= uint64(b&((1<<(8-l))-1)) << (8 * l)
	}

	for i := range l {
		b, err := r.ReadByte()
		if err != nil {
			return 0, fmt.Errorf("readUint64: ReadByte error: %w", err)
		}

		v |= uint64(b) << (8 * i)
	}

	return v, nil
}

// readUint64Bounded is for values that are ultimately used as the length
// argument of a slice. Fuzzing can generate lengths that will cause a panic.
func readUint64Bounded(r io.ByteReader, nonZero bool) (uint64, error) {
	v, err := readUint64(r)
	if err != nil {
		return 0, err
	}

	if err := checkUint64(v, nonZero); err != nil {
		return 0, err
	}

	return v, nil
}

func readBool(r io.ByteReader, count uint64) ([]bool, error) {
	if err := checkUint64(count, true); err != nil {
		return nil, err
	}

	// Preallocate with a maximum initial capacity of 1024 to prevent memory DoS
	// on invalid high counts. Growth via append has negligible performance impact
	// because 7z decompression CPU cycles dominate total execution time.
	defined := make([]bool, 0, min(count, 1024))

	var b, mask byte
	for range count {
		if mask == 0 {
			var err error

			b, err = r.ReadByte()
			if err != nil {
				return nil, fmt.Errorf("readBool: ReadByte error: %w", err)
			}

			mask = 0x80
		}

		defined = append(defined, (b&mask) != 0)
		mask >>= 1
	}

	return defined, nil
}

func readOptionalBool(r io.ByteReader, count uint64) ([]bool, error) {
	if err := checkUint64(count, true); err != nil {
		return nil, err
	}

	all, err := r.ReadByte()
	if err != nil {
		return nil, fmt.Errorf("readOptionalBool: ReadByte error: %w", err)
	}

	if all == 0 {
		return readBool(r, count)
	}

	defined := make([]bool, count)
	for i := range defined {
		defined[i] = true
	}

	return defined, nil
}

func readSizes(r io.ByteReader, count uint64) ([]uint64, error) {
	if err := checkUint64(count, true); err != nil {
		return nil, err
	}

	// Preallocate with a maximum initial capacity of 1024 to prevent memory DoS
	// on invalid high counts. Growth via append has negligible performance impact
	// because 7z decompression CPU cycles dominate total execution time.
	sizes := make([]uint64, 0, min(count, 1024))

	for range count {
		size, err := readUint64(r)
		if err != nil {
			return nil, err
		}

		sizes = append(sizes, size)
	}

	return sizes, nil
}

func readCRC(r util.Reader, count uint64) ([]uint32, error) {
	if err := checkUint64(count, true); err != nil {
		return nil, err
	}

	defined, err := readOptionalBool(r, count)
	if err != nil {
		return nil, err
	}

	crcs := make([]uint32, count)

	for i := range defined {
		if defined[i] {
			if err := binary.Read(r, binary.LittleEndian, &crcs[i]); err != nil {
				return nil, fmt.Errorf("readCRC: Read error: %w", err)
			}
		}
	}

	return crcs, nil
}

//nolint:cyclop
func readPackInfo(r util.Reader) (*packInfo, error) {
	p := new(packInfo)

	var err error

	p.position, err = readUint64(r)
	if err != nil {
		return nil, err
	}

	p.streams, err = readUint64Bounded(r, true)
	if err != nil {
		return nil, err
	}

	id, err := r.ReadByte()
	if err != nil {
		return nil, fmt.Errorf("readPackInfo: ReadByte error: %w", err)
	}

	if id == idSize {
		if p.size, err = readSizes(r, p.streams); err != nil {
			return nil, err
		}

		id, err = r.ReadByte()
		if err != nil {
			return nil, fmt.Errorf("readPackInfo: ReadByte error: %w", err)
		}
	}

	if id == idCRC {
		if p.digest, err = readCRC(r, p.streams); err != nil {
			return nil, err
		}

		id, err = r.ReadByte()
		if err != nil {
			return nil, fmt.Errorf("readPackInfo: ReadByte error: %w", err)
		}
	}

	if id != idEnd {
		return nil, errUnexpectedID
	}

	return p, nil
}

//nolint:cyclop
func readCoder(r util.Reader) (*coder, error) {
	c := new(coder)

	v, err := r.ReadByte()
	if err != nil {
		return nil, fmt.Errorf("readCoder: ReadByte error: %w", err)
	}

	c.id = make([]byte, v&0xf)
	if n, err := r.Read(c.id); err != nil || n != int(v&0xf) {
		if err != nil {
			return nil, fmt.Errorf("readCoder: Read error: %w", err)
		}

		return nil, errIncompleteRead
	}

	if v&0x10 != 0 {
		c.in, err = readUint64Bounded(r, true)
		if err != nil {
			return nil, err
		}

		c.out, err = readUint64Bounded(r, true)
		if err != nil {
			return nil, err
		}
	} else {
		c.in, c.out = 1, 1
	}

	if v&0x20 != 0 {
		size, err := readUint64Bounded(r, true)
		if err != nil {
			return nil, err
		}

		c.properties = make([]byte, size)
		if n, err := r.Read(c.properties); err != nil || uint64(n) != size { //nolint:gosec
			if err != nil {
				return nil, fmt.Errorf("readCoder: Read error: %w", err)
			}

			return nil, errIncompleteRead
		}
	}

	return c, nil
}

//nolint:cyclop,funlen
func readFolder(r util.Reader) (*folder, error) {
	f := new(folder)

	coders, err := readUint64Bounded(r, true)
	if err != nil {
		return nil, err
	}

	f.coder = make([]*coder, 0, min(coders, 16))

	for range coders {
		coder, err := readCoder(r)
		if err != nil {
			return nil, err
		}

		f.coder = append(f.coder, coder)

		f.in += coder.in
		f.out += coder.out
	}

	if err := checkUint64(f.in, true); err != nil {
		return nil, err
	}

	if err := checkUint64(f.out, true); err != nil {
		return nil, err
	}

	if f.in < f.out {
		return nil, errInvalidBindPair
	}

	bindPairs := f.out - 1

	f.bindPair = make([]*bindPair, 0, min(bindPairs, 16))

	for range bindPairs {
		in, err := readUint64Bounded(r, false)
		if err != nil {
			return nil, err
		}

		out, err := readUint64Bounded(r, false)
		if err != nil {
			return nil, err
		}

		f.bindPair = append(f.bindPair, &bindPair{
			in:  in,
			out: out,
		})
	}

	f.packedStreams = f.in - bindPairs

	if f.packedStreams == 1 {
		f.packed = []uint64{}
		for i := range f.in {
			if f.findInBindPair(i) == nil {
				f.packed = append(f.packed, i)
			}
		}
	} else {
		f.packed = make([]uint64, f.packedStreams)
		for i := range f.packedStreams {
			if f.packed[i], err = readUint64(r); err != nil {
				return nil, err
			}
		}
	}

	return f, nil
}

//nolint:cyclop,funlen,gocognit
func readUnpackInfo(r util.Reader) (*unpackInfo, error) {
	u := new(unpackInfo)

	if id, err := r.ReadByte(); err != nil || id != idFolder {
		if err != nil {
			return nil, fmt.Errorf("readUnpackInfo: ReadByte error: %w", err)
		}

		return nil, errUnexpectedID
	}

	folders, err := readUint64Bounded(r, true)
	if err != nil {
		return nil, err
	}

	external, err := r.ReadByte()
	if err != nil {
		return nil, fmt.Errorf("readUnpackInfo: ReadByte error: %w", err)
	}

	if external > 0 {
		/*
			_, err := readUint64(r)
			if err != nil {
				return nil, err
			}
		*/
		// TODO Apparently we seek to this read offset and read the
		// folder information from there. Not clear if the offset is
		// absolute for the whole file, or relative to some known
		// position in the file. Cowardly waiting for an example
		return nil, errors.New("sevenzip: TODO readUnpackInfo external") //nolint:err113
	}

	u.folder = make([]*folder, 0, min(folders, 1024))

	for range folders {
		folder, err := readFolder(r)
		if err != nil {
			return nil, err
		}

		u.folder = append(u.folder, folder)
	}

	if id, err := r.ReadByte(); err != nil || id != idCodersUnpackSize {
		if err != nil {
			return nil, fmt.Errorf("readUnpackInfo: ReadByte error: %w", err)
		}

		return nil, errUnexpectedID
	}

	for _, f := range u.folder {
		total := uint64(0)
		for _, c := range f.coder {
			total += c.out
		}

		if err := checkUint64(total, true); err != nil {
			return nil, err
		}

		f.size = make([]uint64, total)
		for i := range f.size {
			if f.size[i], err = readUint64(r); err != nil {
				return nil, err
			}
		}
	}

	id, err := r.ReadByte()
	if err != nil {
		return nil, fmt.Errorf("readUnpackInfo: ReadByte error: %w", err)
	}

	if id == idCRC {
		if u.digest, err = readCRC(r, folders); err != nil {
			return nil, err
		}

		id, err = r.ReadByte()
		if err != nil {
			return nil, fmt.Errorf("readUnpackInfo: ReadByte error: %w", err)
		}
	}

	if id != idEnd {
		return nil, errUnexpectedID
	}

	return u, nil
}

//nolint:cyclop,funlen,gocognit
func readSubStreamsInfo(r util.Reader, folder []*folder) (*subStreamsInfo, error) {
	s := new(subStreamsInfo)

	id, err := r.ReadByte()
	if err != nil {
		return nil, fmt.Errorf("readSubStreamsInfo: ReadByte error: %w", err)
	}

	s.streams = make([]uint64, len(folder))
	if id == idNumUnpackStream {
		for i := range s.streams {
			if s.streams[i], err = readUint64(r); err != nil {
				return nil, err
			}
		}

		id, err = r.ReadByte()
		if err != nil {
			return nil, fmt.Errorf("readSubStreamsInfo: ReadByte error: %w", err)
		}
	} else {
		for i := range s.streams {
			s.streams[i] = 1
		}
	}

	// Count the files in each stream
	files := uint64(0)
	for _, v := range s.streams {
		files += v
	}

	if err := checkUint64(files, false); err != nil {
		return nil, err
	}

	if id == idSize {
		s.size = make([]uint64, files)
		k := 0

		for i := range s.streams {
			if s.streams[i] == 0 {
				continue
			}

			total := uint64(0)

			for j := uint64(1); j < s.streams[i]; j++ {
				if s.size[k], err = readUint64(r); err != nil {
					return nil, err
				}

				total += s.size[k]
				k++
			}

			s.size[k] = folder[i].unpackSize() - total
			k++
		}

		id, err = r.ReadByte()
		if err != nil {
			return nil, fmt.Errorf("readSubStreamsInfo: ReadByte error: %w", err)
		}
	}

	if id == idCRC {
		if s.digest, err = readCRC(r, files); err != nil {
			return nil, err
		}

		id, err = r.ReadByte()
		if err != nil {
			return nil, fmt.Errorf("readSubStreamsInfo: ReadByte error: %w", err)
		}
	}

	if id != idEnd {
		return nil, errUnexpectedID
	}

	return s, nil
}

//nolint:cyclop
func readStreamsInfo(r util.Reader) (*streamsInfo, error) {
	s := new(streamsInfo)

	id, err := r.ReadByte()
	if err != nil {
		return nil, fmt.Errorf("readStreamsInfo: ReadByte error: %w", err)
	}

	if id == idPackInfo {
		if s.packInfo, err = readPackInfo(r); err != nil {
			return nil, err
		}

		id, err = r.ReadByte()
		if err != nil {
			return nil, fmt.Errorf("readStreamsInfo: ReadByte error: %w", err)
		}
	}

	if id == idUnpackInfo {
		if s.unpackInfo, err = readUnpackInfo(r); err != nil {
			return nil, err
		}

		id, err = r.ReadByte()
		if err != nil {
			return nil, fmt.Errorf("readStreamsInfo: ReadByte error: %w", err)
		}
	}

	if id == idSubStreamsInfo {
		if s.unpackInfo == nil {
			return nil, errMissingUnpackInfo
		}

		if s.subStreamsInfo, err = readSubStreamsInfo(r, s.unpackInfo.folder); err != nil {
			return nil, err
		}

		id, err = r.ReadByte()
		if err != nil {
			return nil, fmt.Errorf("readStreamsInfo: ReadByte error: %w", err)
		}
	}

	if id != idEnd {
		return nil, errUnexpectedID
	}

	return s, nil
}

func readTimes(r util.Reader, count uint64) ([]time.Time, error) {
	if err := checkUint64(count, true); err != nil {
		return nil, err
	}

	defined, err := readOptionalBool(r, count)
	if err != nil {
		return nil, err
	}

	external, err := r.ReadByte()
	if err != nil {
		return nil, fmt.Errorf("readTimes: ReadByte error: %w", err)
	}

	if external > 0 {
		/*
			_, err := readUint64(r)
			if err != nil {
				return nil, err
			}
		*/
		// TODO Apparently we seek to this read offset and read the
		// folder information from there. Not clear if the offset is
		// absolute for the whole file, or relative to some known
		// position in the file. Cowardly waiting for an example
		return nil, errors.New("sevenzip: TODO readTimes external") //nolint:err113
	}

	times := make([]time.Time, count)

	for i := range defined {
		if defined[i] {
			var ft windows.Filetime
			if err := binary.Read(r, binary.LittleEndian, &ft); err != nil {
				return nil, fmt.Errorf("readTimes: Read error: %w", err)
			}

			times[i] = time.Unix(0, ft.Nanoseconds()).UTC()
		}
	}

	return times, nil
}

func splitNull(data []byte, atEOF bool) (advance int, token []byte, err error) {
	if atEOF && len(data) == 0 {
		return 0, nil, nil
	}

	if i := bytes.IndexRune(data, rune(0)); i >= 0 {
		return i + 1, data[0:i], nil
	}

	if atEOF {
		return len(data), data, nil
	}

	return
}

func readNames(r util.Reader, count, length uint64) ([]string, error) {
	if err := checkUint64(count, true); err != nil {
		return nil, err
	}

	external, err := r.ReadByte()
	if err != nil {
		return nil, fmt.Errorf("readNames: ReadByte error: %w", err)
	}

	if external > 0 {
		/*
			_, err := readUint64(r)
			if err != nil {
				return nil, err
			}
		*/
		// TODO Apparently we seek to this read offset and read the
		// folder information from there. Not clear if the offset is
		// absolute for the whole file, or relative to some known
		// position in the file. Cowardly waiting for an example
		return nil, errors.New("sevenzip: TODO readNames external") //nolint:err113
	}

	utf16le := unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM)
	scanner := bufio.NewScanner(transform.NewReader(io.LimitReader(r, int64(length-1)), utf16le.NewDecoder())) //nolint:gosec,lll
	scanner.Split(splitNull)

	names, i := make([]string, 0, count), uint64(0)
	for scanner.Scan() {
		names = append(names, scanner.Text())
		i++
	}

	if err = scanner.Err(); err != nil {
		return nil, fmt.Errorf("readNames: Scan error: %w", err)
	}

	if i != count {
		return nil, errWrongNumberOfFilenames
	}

	return names, nil
}

func readAttributes(r util.Reader, count uint64) ([]uint32, error) {
	if err := checkUint64(count, true); err != nil {
		return nil, err
	}

	defined, err := readOptionalBool(r, count)
	if err != nil {
		return nil, err
	}

	external, err := r.ReadByte()
	if err != nil {
		return nil, fmt.Errorf("readAttributes: ReadByte error: %w", err)
	}

	if external > 0 {
		/*
			_, err := readUint64(r)
			if err != nil {
				return nil, err
			}
		*/
		// TODO Apparently we seek to this read offset and read the
		// folder information from there. Not clear if the offset is
		// absolute for the whole file, or relative to some known
		// position in the file. Cowardly waiting for an example
		return nil, errors.New("sevenzip: TODO readAttributes external") //nolint:err113
	}

	attributes := make([]uint32, count)

	for i := range defined {
		if defined[i] {
			if err := binary.Read(r, binary.LittleEndian, &attributes[i]); err != nil {
				return nil, fmt.Errorf("readAttributes: Read error: %w", err)
			}
		}
	}

	return attributes, nil
}

//nolint:cyclop,funlen,gocognit,gocyclo
func readFilesInfo(r util.Reader) (*filesInfo, error) {
	f := new(filesInfo)

	files, err := readUint64Bounded(r, true)
	if err != nil {
		return nil, err
	}

	f.file = make([]FileHeader, files)

	var emptyStreams uint64

	for {
		property, err := r.ReadByte()
		if err != nil {
			return nil, fmt.Errorf("readFilesInfo: ReadByte error: %w", err)
		}

		if property == idEnd {
			break
		}

		length, err := readUint64(r)
		if err != nil {
			return nil, err
		}

		switch property {
		case idEmptyStream:
			empty, err := readBool(r, files)
			if err != nil {
				return nil, err
			}

			for i := range f.file {
				f.file[i].isEmptyStream = empty[i]

				if empty[i] {
					emptyStreams++
				}
			}
		case idEmptyFile:
			empty, err := readBool(r, emptyStreams)
			if err != nil {
				return nil, err
			}

			j := 0

			for i := range f.file {
				if f.file[i].isEmptyStream {
					f.file[i].isEmptyFile = empty[j]
					j++
				}
			}
		case idCTime:
			times, err := readTimes(r, files)
			if err != nil {
				return nil, err
			}

			for i, t := range times {
				f.file[i].Created = t
			}
		case idATime:
			times, err := readTimes(r, files)
			if err != nil {
				return nil, err
			}

			for i, t := range times {
				f.file[i].Accessed = t
			}
		case idMTime:
			times, err := readTimes(r, files)
			if err != nil {
				return nil, err
			}

			for i, t := range times {
				f.file[i].Modified = t
			}
		case idName:
			names, err := readNames(r, files, length)
			if err != nil {
				return nil, err
			}

			for i, n := range names {
				f.file[i].Name = n
			}
		case idWinAttributes:
			attributes, err := readAttributes(r, files)
			if err != nil {
				return nil, err
			}

			for i, a := range attributes {
				f.file[i].Attributes = a
			}
		case idStartPos:
			return nil, errors.New("sevenzip: TODO idStartPos") //nolint:err113
		case idDummy:
			if _, err := io.CopyN(io.Discard, r, int64(length)); err != nil { //nolint:gosec
				return nil, fmt.Errorf("readFilesInfo: CopyN error: %w", err)
			}
		default:
			return nil, errUnexpectedID
		}
	}

	return f, nil
}

//nolint:cyclop,funlen
func readHeader(r util.Reader) (*header, error) {
	h := new(header)

	id, err := r.ReadByte()
	if err != nil {
		return nil, fmt.Errorf("readHeader: ReadByte error: %w", err)
	}

	if id == idArchiveProperties {
		/*
			id, err = r.ReadByte()
			if err != nil {
				return nil, fmt.Errorf("readHeader: ReadByte error: %w", err)
			}
		*/
		return nil, errors.New("sevenzip: TODO idArchiveProperties") //nolint:err113
	}

	if id == idAdditionalStreamsInfo {
		/*
			id, err = r.ReadByte()
			if err != nil {
				return nil, fmt.Errorf("readHeader: ReadByte error: %w", err)
			}
		*/
		return nil, errors.New("sevenzip: TODO idAdditionalStreamsInfo") //nolint:err113
	}

	if id == idMainStreamsInfo {
		if h.streamsInfo, err = readStreamsInfo(r); err != nil {
			return nil, err
		}

		id, err = r.ReadByte()
		if err != nil {
			return nil, fmt.Errorf("readHeader: ReadByte error: %w", err)
		}
	}

	if id == idFilesInfo {
		if h.filesInfo, err = readFilesInfo(r); err != nil {
			return nil, err
		}

		id, err = r.ReadByte()
		if err != nil {
			return nil, fmt.Errorf("readHeader: ReadByte error: %w", err)
		}
	}

	if id != idEnd {
		return nil, errUnexpectedID
	}

	if h.streamsInfo == nil || h.filesInfo == nil {
		return h, nil
	}

	j := 0

	for i := range h.filesInfo.file {
		if h.filesInfo.file[i].isEmptyStream {
			continue
		}

		_, h.filesInfo.file[i].UncompressedSize, h.filesInfo.file[i].CRC32 = h.streamsInfo.FileFolderAndSize(j)
		j++
	}

	return h, nil
}

func readEncodedHeader(r util.Reader) (*header, error) {
	if id, err := r.ReadByte(); err != nil || id != idHeader {
		if err != nil {
			return nil, fmt.Errorf("readEncodedHeader: ReadByte error: %w", err)
		}

		return nil, errUnexpectedID
	}

	header, err := readHeader(r)
	if err != nil {
		return nil, err
	}

	return header, nil
}
