package handler

import (
	"bytes"
	"context"
	"crypto/rand"
	"crypto/sha256"
	"fmt"
	"io"
	"strings"

	"git.frostfs.info/TrueCloudLab/frostfs-http-gw/utils"
	"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/bearer"
	"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/checksum"
	apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status"
	"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container"
	"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/acl"
	cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
	"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object"
	oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
	"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user"
	"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
)

type TestFrostFS struct {
	objects    map[string]*object.Object
	containers map[string]*container.Container
	accessList map[string]bool
	key        *keys.PrivateKey
}

func NewTestFrostFS(key *keys.PrivateKey) *TestFrostFS {
	return &TestFrostFS{
		objects:    make(map[string]*object.Object),
		containers: make(map[string]*container.Container),
		accessList: make(map[string]bool),
		key:        key,
	}
}

func (t *TestFrostFS) ContainerID(name string) (*cid.ID, error) {
	for id, cnr := range t.containers {
		if container.Name(*cnr) == name {
			var cnrID cid.ID
			return &cnrID, cnrID.DecodeString(id)
		}
	}
	return nil, fmt.Errorf("not found")
}

func (t *TestFrostFS) SetContainer(cnrID cid.ID, cnr *container.Container) {
	t.containers[cnrID.EncodeToString()] = cnr
}

// AllowUserOperation grants access to object operations.
// Empty userID and objID means any user and object respectively.
func (t *TestFrostFS) AllowUserOperation(cnrID cid.ID, userID user.ID, op acl.Op, objID oid.ID) {
	t.accessList[fmt.Sprintf("%s/%s/%s/%s", cnrID, userID, op, objID)] = true
}

func (t *TestFrostFS) Container(_ context.Context, prm PrmContainer) (*container.Container, error) {
	for k, v := range t.containers {
		if k == prm.ContainerID.EncodeToString() {
			return v, nil
		}
	}

	return nil, fmt.Errorf("container not found %s", prm.ContainerID)
}

func (t *TestFrostFS) requestOwner(btoken *bearer.Token) user.ID {
	if btoken != nil {
		return bearer.ResolveIssuer(*btoken)
	}

	var owner user.ID
	user.IDFromKey(&owner, t.key.PrivateKey.PublicKey)
	return owner
}

func (t *TestFrostFS) retrieveObject(addr oid.Address, btoken *bearer.Token) (*object.Object, error) {
	sAddr := addr.EncodeToString()

	if obj, ok := t.objects[sAddr]; ok {
		owner := t.requestOwner(btoken)

		if !t.isAllowed(addr.Container(), owner, acl.OpObjectGet, addr.Object()) {
			return nil, ErrAccessDenied
		}

		return obj, nil
	}

	return nil, fmt.Errorf("%w: %s", &apistatus.ObjectNotFound{}, addr)
}

func (t *TestFrostFS) HeadObject(_ context.Context, prm PrmObjectHead) (*object.Object, error) {
	return t.retrieveObject(prm.Address, prm.BearerToken)
}

func (t *TestFrostFS) GetObject(_ context.Context, prm PrmObjectGet) (*Object, error) {
	obj, err := t.retrieveObject(prm.Address, prm.BearerToken)
	if err != nil {
		return nil, err
	}

	return &Object{
		Header:  *obj,
		Payload: io.NopCloser(bytes.NewReader(obj.Payload())),
	}, nil
}

func (t *TestFrostFS) RangeObject(_ context.Context, prm PrmObjectRange) (io.ReadCloser, error) {
	obj, err := t.retrieveObject(prm.Address, prm.BearerToken)
	if err != nil {
		return nil, err
	}

	off := prm.PayloadRange[0]
	payload := obj.Payload()[off : off+prm.PayloadRange[1]]
	return io.NopCloser(bytes.NewReader(payload)), nil
}

func (t *TestFrostFS) CreateObject(_ context.Context, prm PrmObjectCreate) (oid.ID, error) {
	b := make([]byte, 32)
	if _, err := io.ReadFull(rand.Reader, b); err != nil {
		return oid.ID{}, err
	}
	var id oid.ID
	id.SetSHA256(sha256.Sum256(b))
	prm.Object.SetID(id)

	attrs := prm.Object.Attributes()
	if prm.ClientCut {
		a := object.NewAttribute()
		a.SetKey("s3-client-cut")
		a.SetValue("true")
		attrs = append(attrs, *a)
	}

	prm.Object.SetAttributes(attrs...)

	if prm.Payload != nil {
		all, err := io.ReadAll(prm.Payload)
		if err != nil {
			return oid.ID{}, err
		}
		prm.Object.SetPayload(all)
		prm.Object.SetPayloadSize(uint64(len(all)))
		var hash checksum.Checksum
		checksum.Calculate(&hash, checksum.SHA256, all)
		prm.Object.SetPayloadChecksum(hash)
	}

	cnrID, _ := prm.Object.ContainerID()
	objID, _ := prm.Object.ID()

	owner := t.requestOwner(prm.BearerToken)

	if !t.isAllowed(cnrID, owner, acl.OpObjectPut, objID) {
		return oid.ID{}, ErrAccessDenied
	}

	addr := newAddress(cnrID, objID)
	t.objects[addr.EncodeToString()] = prm.Object
	return objID, nil
}

type resObjectSearchMock struct {
	res []oid.ID
}

func (r *resObjectSearchMock) Read(buf []oid.ID) (int, error) {
	for i := range buf {
		if i > len(r.res)-1 {
			return len(r.res), io.EOF
		}
		buf[i] = r.res[i]
	}

	r.res = r.res[len(buf):]

	return len(buf), nil
}

func (r *resObjectSearchMock) Iterate(f func(oid.ID) bool) error {
	for _, id := range r.res {
		if f(id) {
			return nil
		}
	}

	return nil
}

func (r *resObjectSearchMock) Close() {}

func (t *TestFrostFS) SearchObjects(_ context.Context, prm PrmObjectSearch) (ResObjectSearch, error) {
	if !t.isAllowed(prm.Container, t.requestOwner(prm.BearerToken), acl.OpObjectSearch, oid.ID{}) {
		return nil, ErrAccessDenied
	}

	cidStr := prm.Container.EncodeToString()
	var res []oid.ID

	if len(prm.Filters) == 1 { // match root filter
		for k, v := range t.objects {
			if strings.Contains(k, cidStr) {
				id, _ := v.ID()
				res = append(res, id)
			}
		}
		return &resObjectSearchMock{res: res}, nil
	}

	filter := prm.Filters[1]
	if len(prm.Filters) != 2 ||
		filter.Operation() != object.MatchCommonPrefix && filter.Operation() != object.MatchStringEqual {
		return nil, fmt.Errorf("usupported filters")
	}

	for k, v := range t.objects {
		if strings.Contains(k, cidStr) && isMatched(v.Attributes(), filter) {
			id, _ := v.ID()
			res = append(res, id)
		}
	}

	return &resObjectSearchMock{res: res}, nil
}

func (t *TestFrostFS) InitMultiObjectReader(context.Context, PrmInitMultiObjectReader) (io.Reader, error) {
	return nil, nil
}

func isMatched(attributes []object.Attribute, filter object.SearchFilter) bool {
	for _, attr := range attributes {
		if attr.Key() == filter.Header() {
			switch filter.Operation() {
			case object.MatchStringEqual:
				return attr.Value() == filter.Value()
			case object.MatchCommonPrefix:
				return strings.HasPrefix(attr.Value(), filter.Value())
			default:
				return false
			}
		}
	}

	return false
}

func (t *TestFrostFS) GetEpochDurations(context.Context) (*utils.EpochDurations, error) {
	return &utils.EpochDurations{
		CurrentEpoch:  10,
		MsPerBlock:    1000,
		BlockPerEpoch: 100,
	}, nil
}

func (t *TestFrostFS) isAllowed(cnrID cid.ID, userID user.ID, op acl.Op, objID oid.ID) bool {
	keysToCheck := []string{
		fmt.Sprintf("%s/%s/%s/%s", cnrID, userID, op, objID),
		fmt.Sprintf("%s/%s/%s/%s", cnrID, userID, op, oid.ID{}),
		fmt.Sprintf("%s/%s/%s/%s", cnrID, user.ID{}, op, objID),
		fmt.Sprintf("%s/%s/%s/%s", cnrID, user.ID{}, op, oid.ID{}),
	}

	for _, key := range keysToCheck {
		if t.accessList[key] {
			return true
		}
	}
	return false
}