package tree

import (
	"context"
	"fmt"
	"strings"

	"git.frostfs.info/TrueCloudLab/frostfs-http-gw/internal/api"
	"git.frostfs.info/TrueCloudLab/frostfs-http-gw/internal/api/layer"
	cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
	oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
)

type (
	Tree struct {
		service ServiceClient
	}

	// ServiceClient is a client to interact with tree service.
	// Each method must return ErrNodeNotFound or ErrNodeAccessDenied if relevant.
	ServiceClient interface {
		GetNodes(ctx context.Context, p *GetNodesParams) ([]NodeResponse, error)
	}

	treeNode struct {
		ObjID oid.ID
		Meta  map[string]string
	}

	GetNodesParams struct {
		CnrID      cid.ID
		TreeID     string
		Path       []string
		Meta       []string
		LatestOnly bool
		AllAttrs   bool
	}
)

var (
	// ErrNodeNotFound is returned from ServiceClient in case of not found error.
	ErrNodeNotFound = layer.ErrNodeNotFound

	// ErrNodeAccessDenied is returned from ServiceClient service in case of access denied error.
	ErrNodeAccessDenied = layer.ErrNodeAccessDenied
)

const (
	FileNameKey = "FileName"
)

const (
	oidKV = "OID"

	// keys for delete marker nodes.
	isDeleteMarkerKV = "IsDeleteMarker"

	// versionTree -- ID of a tree with object versions.
	versionTree = "version"

	separator = "/"
)

// NewTree creates instance of Tree using provided address and create grpc connection.
func NewTree(service ServiceClient) *Tree {
	return &Tree{service: service}
}

type Meta interface {
	GetKey() string
	GetValue() []byte
}

type NodeResponse interface {
	GetMeta() []Meta
	GetTimestamp() uint64
}

func newTreeNode(nodeInfo NodeResponse) (*treeNode, error) {
	treeNode := &treeNode{
		Meta: make(map[string]string, len(nodeInfo.GetMeta())),
	}

	for _, kv := range nodeInfo.GetMeta() {
		switch kv.GetKey() {
		case oidKV:
			if err := treeNode.ObjID.DecodeString(string(kv.GetValue())); err != nil {
				return nil, err
			}
		default:
			treeNode.Meta[kv.GetKey()] = string(kv.GetValue())
		}
	}

	return treeNode, nil
}

func (n *treeNode) Get(key string) (string, bool) {
	value, ok := n.Meta[key]
	return value, ok
}

func (n *treeNode) FileName() (string, bool) {
	value, ok := n.Meta[FileNameKey]
	return value, ok
}

func newNodeVersion(node NodeResponse) (*api.NodeVersion, error) {
	treeNode, err := newTreeNode(node)
	if err != nil {
		return nil, fmt.Errorf("invalid tree node: %w", err)
	}

	return newNodeVersionFromTreeNode(treeNode), nil
}

func newNodeVersionFromTreeNode(treeNode *treeNode) *api.NodeVersion {
	_, isDeleteMarker := treeNode.Get(isDeleteMarkerKV)

	version := &api.NodeVersion{
		BaseNodeVersion: api.BaseNodeVersion{
			OID: treeNode.ObjID,
		},
		DeleteMarker: isDeleteMarker,
	}

	return version
}

func (c *Tree) GetLatestVersion(ctx context.Context, cnrID *cid.ID, objectName string) (*api.NodeVersion, error) {
	meta := []string{oidKV, isDeleteMarkerKV}
	path := pathFromName(objectName)

	p := &GetNodesParams{
		CnrID:      *cnrID,
		TreeID:     versionTree,
		Path:       path,
		Meta:       meta,
		LatestOnly: false,
		AllAttrs:   false,
	}
	nodes, err := c.service.GetNodes(ctx, p)
	if err != nil {
		return nil, err
	}

	latestNode, err := getLatestNode(nodes)
	if err != nil {
		return nil, err
	}

	return newNodeVersion(latestNode)
}

func getLatestNode(nodes []NodeResponse) (NodeResponse, error) {
	var (
		maxCreationTime uint64
		targetIndexNode = -1
	)

	for i, node := range nodes {
		currentCreationTime := node.GetTimestamp()
		if checkExistOID(node.GetMeta()) && currentCreationTime > maxCreationTime {
			maxCreationTime = currentCreationTime
			targetIndexNode = i
		}
	}

	if targetIndexNode == -1 {
		return nil, layer.ErrNodeNotFound
	}

	return nodes[targetIndexNode], nil
}

func checkExistOID(meta []Meta) bool {
	for _, kv := range meta {
		if kv.GetKey() == "OID" {
			return true
		}
	}

	return false
}

// pathFromName splits name by '/'.
func pathFromName(objectName string) []string {
	return strings.Split(objectName, separator)
}