package blobtree

import (
	"crypto/sha256"
	"encoding/binary"
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"strconv"

	cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
	oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
)

const (
	defaultVersion = 0

	sizeOfVersion     = 1
	sizeOfCount       = 8
	sizeOfDataLength  = 8
	sizeOfContainerID = sha256.Size
	sizeOfObjectID    = sha256.Size
)

var (
	errFileToSmall                   = errors.New("invalid file content: not enough bytes to read count of records")
	errInvalidFileContentVersion     = errors.New("invalid file content: not enough bytes to read record version")
	errInvalidFileContentContainerID = errors.New("invalid file content: not enough bytes to read container ID")
	errInvalidFileContentObjectID    = errors.New("invalid file content: not enough bytes to read object ID")
	errInvalidFileContentLength      = errors.New("invalid file content: not enough bytes to read data length")
	errInvalidFileContentData        = errors.New("invalid file content: not enough bytes to read data")
)

type objectData struct {
	Version byte
	Address oid.Address
	Data    []byte
}

func (b *BlobTree) readFileContent(path string) ([]objectData, error) {
	rawData, err := os.ReadFile(path)
	if err != nil {
		if os.IsNotExist(err) {
			return []objectData{}, nil
		}
		return nil, err
	}
	return b.unmarshalSlice(rawData)
}

func (b *BlobTree) unmarshalSlice(data []byte) ([]objectData, error) {
	if len(data) < sizeOfCount {
		return nil, errFileToSmall
	}
	count := binary.LittleEndian.Uint64(data[:8])
	result := make([]objectData, 0, count)

	data = data[sizeOfCount:]
	var idx uint64
	for idx = 0; idx < count; idx++ {
		record, read, err := b.unmarshalRecord(data)
		if err != nil {
			return nil, err
		}
		result = append(result, record)
		data = data[read:]
	}

	return result, nil
}

func (b *BlobTree) unmarshalRecord(data []byte) (objectData, uint64, error) {
	if len(data) < sizeOfVersion {
		return objectData{}, 0, errInvalidFileContentVersion
	}
	var result objectData
	var read uint64
	result.Version = data[0]
	if result.Version != defaultVersion {
		return objectData{}, 0, fmt.Errorf("invalid file content: unknown version %d", result.Version)
	}
	read += sizeOfVersion

	if len(data[read:]) < sizeOfContainerID {
		return objectData{}, 0, errInvalidFileContentContainerID
	}
	var contID cid.ID
	if err := contID.Decode(data[read : read+sizeOfContainerID]); err != nil {
		return objectData{}, 0, fmt.Errorf("invalid file content: failed to read container ID: %w", err)
	}
	read += sizeOfContainerID

	if len(data[read:]) < sizeOfObjectID {
		return objectData{}, 0, errInvalidFileContentObjectID
	}
	var objID oid.ID
	if err := objID.Decode(data[read : read+sizeOfObjectID]); err != nil {
		return objectData{}, 0, fmt.Errorf("invalid file content: failed to read object ID: %w", err)
	}
	read += sizeOfObjectID

	result.Address.SetContainer(contID)
	result.Address.SetObject(objID)

	if len(data[read:]) < sizeOfDataLength {
		return objectData{}, 0, errInvalidFileContentLength
	}
	dataLength := binary.LittleEndian.Uint64(data[read : read+sizeOfDataLength])
	read += sizeOfDataLength

	if uint64(len(data[read:])) < dataLength {
		return objectData{}, 0, errInvalidFileContentData
	}
	result.Data = make([]byte, dataLength)
	copy(result.Data, data[read:read+dataLength])
	read += dataLength

	return result, read, nil
}

func (b *BlobTree) saveContentToFile(records []objectData, path string) (uint64, error) {
	data, err := b.marshalSlice(records)
	if err != nil {
		return 0, err
	}
	return uint64(len(data)), b.writeFile(path, data)
}

func (b *BlobTree) writeFile(p string, data []byte) error {
	f, err := os.OpenFile(p, os.O_WRONLY|os.O_CREATE|os.O_TRUNC|os.O_EXCL|os.O_SYNC, b.cfg.permissions)
	if err != nil {
		return err
	}
	_, err = f.Write(data)
	if err1 := f.Close(); err1 != nil && err == nil {
		err = err1
	}
	return err
}

func (b *BlobTree) marshalSlice(records []objectData) ([]byte, error) {
	buf := make([]byte, b.estimateSize(records))
	result := buf
	binary.LittleEndian.PutUint64(buf, uint64(len(records)))
	buf = buf[sizeOfCount:]
	for _, record := range records {
		written := b.marshalRecord(record, buf)
		buf = buf[written:]
	}
	return result, nil
}

func (b *BlobTree) marshalRecord(record objectData, dst []byte) uint64 {
	var written uint64

	dst[0] = record.Version
	dst = dst[sizeOfVersion:]
	written += sizeOfVersion

	record.Address.Container().Encode(dst)
	dst = dst[sizeOfContainerID:]
	written += sizeOfContainerID

	record.Address.Object().Encode(dst)
	dst = dst[sizeOfObjectID:]
	written += sizeOfObjectID

	binary.LittleEndian.PutUint64(dst, uint64(len(record.Data)))
	dst = dst[sizeOfDataLength:]
	written += sizeOfDataLength

	copy(dst, record.Data)
	written += uint64(len(record.Data))

	return written
}

func (b *BlobTree) estimateSize(records []objectData) uint64 {
	var result uint64
	result += sizeOfCount
	for _, record := range records {
		result += (sizeOfVersion + sizeOfContainerID + sizeOfObjectID + sizeOfDataLength)
		result += uint64(len(record.Data))
	}
	return result
}

func (b *BlobTree) getFilePath(dir string, idx uint64) string {
	return filepath.Join(dir, strconv.FormatUint(idx, 16))
}