package blobtree

import (
	"context"
	"os"
	"path/filepath"
	"time"

	"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/local_object_storage/blobstor/common"
	"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/local_object_storage/util/logicerr"
	"git.frostfs.info/TrueCloudLab/frostfs-observability/tracing"
	apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status"
	oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
	"go.opentelemetry.io/otel/attribute"
	"go.opentelemetry.io/otel/trace"
)

func (b *BlobTree) Delete(ctx context.Context, prm common.DeletePrm) (common.DeleteRes, error) {
	var (
		success   = false
		startedAt = time.Now()
	)
	defer func() {
		b.cfg.metrics.Delete(time.Since(startedAt), success, prm.StorageID != nil)
	}()

	_, span := tracing.StartSpanFromContext(ctx, "BlobTree.Delete",
		trace.WithAttributes(
			attribute.String("path", b.cfg.rootPath),
			attribute.String("address", prm.Address.EncodeToString()),
			attribute.String("storage_id", string(prm.StorageID)),
		))
	defer span.End()

	if b.cfg.readOnly {
		return common.DeleteRes{}, common.ErrReadOnly
	}

	var res common.DeleteRes
	var err error
	if path, ok := getPathFromStorageID(prm.StorageID); ok {
		res, err = b.deleteFromPath(prm.Address, path)
	} else {
		res, err = b.findAndDelete(prm.Address)
	}
	success = err == nil
	return res, err
}

func (b *BlobTree) deleteFromPath(addr oid.Address, path string) (common.DeleteRes, error) {
	b.fileLock.Lock(path)
	defer b.fileLock.Unlock(path)

	dir, idx, err := b.parsePath(path)
	if err != nil {
		return common.DeleteRes{}, err
	}

	records, err := b.readFileContent(path)
	if err != nil {
		return common.DeleteRes{}, err
	}

	deleteIdx := -1
	for i := range records {
		if records[i].Address.Equals(addr) {
			deleteIdx = i
			break
		}
	}

	if deleteIdx == -1 {
		return common.DeleteRes{}, logicerr.Wrap(new(apistatus.ObjectNotFound))
	}

	if len(records) == 1 {
		err = os.Remove(b.getSystemPath(path))
		if err == nil {
			b.dispatcher.ReturnIdx(dir, idx)
			b.cfg.metrics.DecFilesCount()
		}
		return common.DeleteRes{}, err
	}

	records = append(records[:deleteIdx], records[deleteIdx+1:]...)
	size, err := b.writeToTmpAndRename(records, path)
	if err != nil {
		return common.DeleteRes{}, err
	}
	if size < b.cfg.targetFileSizeBytes {
		b.dispatcher.ReturnIdx(dir, idx)
	}
	return common.DeleteRes{}, nil
}

func (b *BlobTree) findAndDelete(addr oid.Address) (common.DeleteRes, error) {
	dir := b.getDir(addr)
	idx, err := b.findFileIdx(dir, addr)
	if err != nil {
		return common.DeleteRes{}, err
	}
	return b.deleteFromPath(addr, b.getFilePath(dir, idx))
}

func (b *BlobTree) findFileIdx(dir string, addr oid.Address) (uint64, error) {
	entities, err := os.ReadDir(filepath.Join(b.cfg.rootPath, dir))
	if err != nil {
		if os.IsNotExist(err) {
			return 0, logicerr.Wrap(new(apistatus.ObjectNotFound))
		}
		return 0, err
	}
	for _, entity := range entities {
		if entity.IsDir() {
			continue
		}
		if b.isTempFile(entity.Name()) {
			continue
		}
		idx, err := b.parseIdx(entity.Name())
		if err != nil {
			continue
		}
		path := b.getFilePath(dir, idx)
		contains, err := b.fileContainsObject(path, addr)
		if err != nil {
			return 0, err
		}
		if contains {
			return idx, nil
		}
	}
	return 0, logicerr.Wrap(new(apistatus.ObjectNotFound))
}

func (b *BlobTree) fileContainsObject(path string, addr oid.Address) (bool, error) {
	b.fileLock.RLock(path)
	defer b.fileLock.RUnlock(path)

	records, err := b.readFileContent(path)
	if err != nil {
		return false, err
	}

	for i := range records {
		if records[i].Address.Equals(addr) {
			return true, nil
		}
	}
	return false, nil
}