package file

import (
	"context"
	"fmt"

	"github.com/go-faster/errors"

	"go.mau.fi/mautrix-telegram/pkg/gotd/crypto"
	"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
	"go.mau.fi/mautrix-telegram/pkg/gotd/tgerr"
)

func getLocation(loc tg.InputFileLocationClass) (string, error) {
	v, ok := loc.(interface {
		GetLocalID() int
		GetVolumeID() int64
	})
	if !ok {
		return "", tgerr.New(400, tg.ErrFileIDInvalid)
	}

	return fmt.Sprintf("%d_%d", v.GetLocalID(), v.GetVolumeID()), nil
}

func (m *Service) openLocation(loc tg.InputFileLocationClass) (File, error) {
	name, err := getLocation(loc)
	if err != nil {
		return nil, err
	}

	f, err := m.storage.Open(name)
	if err != nil {
		return nil, tgerr.New(400, tg.ErrFileIDInvalid)
	}

	return f, nil
}

func (m *Service) getPart(loc tg.InputFileLocationClass, offset int64, limit int) ([]byte, error) {
	f, err := m.openLocation(loc)
	if err != nil {
		return nil, err
	}

	r := make([]byte, limit)
	n, err := f.ReadAt(r, offset)
	if err != nil {
		return nil, errors.Wrap(err, "read from storage")
	}

	return r[:n], nil
}

func (m *Service) UploadGetFile(ctx context.Context, request *tg.UploadGetFileRequest) (tg.UploadFileClass, error) {
	data, err := m.getPart(request.Location, request.Offset, request.Limit)
	if err != nil {
		return nil, err
	}

	return &tg.UploadFile{
		Type:  &tg.StorageFilePartial{},
		Mtime: 0,
		Bytes: data,
	}, nil
}

func countHashes(data []byte, offset int64, partSize int) []tg.FileHash {
	actions := data
	batchSize := partSize
	batches := make([][]byte, 0, (len(actions)+batchSize-1)/batchSize)

	for batchSize < len(actions) {
		actions, batches = actions[batchSize:], append(batches, actions[0:batchSize:batchSize])
	}
	batches = append(batches, actions)

	currentRange := make([]tg.FileHash, 0, 10)
	for _, batch := range batches {
		currentRange = append(currentRange, tg.FileHash{
			Offset: offset,
			Limit:  partSize,
			Hash:   crypto.SHA256(batch),
		})
		offset += int64(len(batch))
	}
	return currentRange
}

func divAndCeil(a, b int) int {
	r := a / b
	if a%b != 0 {
		r++
	}

	return r
}

// computeBatch computes hash range number for given offset.
func computeBatch(offset int64, rangeSize, partSize int) int {
	// Compute number of parts in partSize from offset.
	parts := divAndCeil(int(offset+1), partSize)
	// Compute number of hash ranges in rangeSize.
	batches := divAndCeil(parts, rangeSize)

	return batches
}

func (m *Service) UploadGetFileHashes(
	ctx context.Context,
	request *tg.UploadGetFileHashesRequest,
) ([]tg.FileHash, error) {
	f, err := m.openLocation(request.Location)
	if err != nil {
		return nil, err
	}

	if request.Offset >= int64(f.Size()) {
		return nil, nil
	}
	partSize := m.hashPartSize
	rangeSize := m.hashRangeSize
	batch := computeBatch(request.Offset, rangeSize, partSize)

	low := (batch - 1) * rangeSize * partSize
	high := batch * rangeSize * partSize

	r := make([]byte, high-low)
	n, err := f.ReadAt(r, int64(low))
	if err != nil {
		return nil, err
	}
	r = r[:n]

	return countHashes(r, int64(low), partSize), nil
}
