package client

import (
	"context"
	"crypto/ecdsa"
	"fmt"

	"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/acl"
	v2object "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/object"
	rpcapi "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc"
	"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc/client"
	v2session "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/session"
	"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/signature"
	"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/bearer"
	"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object"
	"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/session"
)

// PrmObjectPutSingle groups parameters of PutSingle operation.
type PrmObjectPutSingle struct {
	XHeaders []string

	BearerToken *bearer.Token

	Session *session.Object

	Local bool

	CopiesNumber []uint32

	Object *object.Object

	Key *ecdsa.PrivateKey
}

// SetCopiesNumber sets ordered list of minimal required object copies numbers
// per placement vector. List's length MUST equal container's placement vector number,
// otherwise request will fail.
//
// Deprecated: Use PrmObjectPutSingle.CopiesNumber instead.
func (prm *PrmObjectPutSingle) SetCopiesNumber(v []uint32) {
	prm.CopiesNumber = v
}

// UseKey specifies private key to sign the requests.
// If key is not provided, then Client default key is used.
//
// Deprecated: Use PrmObjectPutSingle.Key instead.
func (prm *PrmObjectPutSingle) UseKey(key *ecdsa.PrivateKey) {
	prm.Key = key
}

// WithBearerToken attaches bearer token to be used for the operation.
// Should be called once before any writing steps.
//
// Deprecated: Use PrmObjectPutSingle.BearerToken instead.
func (prm *PrmObjectPutSingle) WithBearerToken(t bearer.Token) {
	prm.BearerToken = &t
}

// WithinSession specifies session within which object should be stored.
// Should be called once before any writing steps.
//
// Deprecated: Use PrmObjectPutSingle.Session instead.
func (prm *PrmObjectPutSingle) WithinSession(t session.Object) {
	prm.Session = &t
}

// ExecuteLocal tells the server to execute the operation locally.
//
// Deprecated: Use PrmObjectPutSingle.Local instead.
func (prm *PrmObjectPutSingle) ExecuteLocal() {
	prm.Local = true
}

// WithXHeaders specifies list of extended headers (string key-value pairs)
// to be attached to the request. Must have an even length.
//
// Slice must not be mutated until the operation completes.
//
// Deprecated: Use PrmObjectPutSingle.XHeaders instead.
func (prm *PrmObjectPutSingle) WithXHeaders(hs ...string) {
	prm.XHeaders = hs
}

// SetObject specifies prepared object to put.
//
// Deprecated: Use PrmObjectPutSingle.Object instead.
func (prm *PrmObjectPutSingle) SetObject(o *v2object.Object) {
	prm.Object = object.NewFromV2(o)
}

// ResObjectPutSingle groups resulting values of PutSingle operation.
type ResObjectPutSingle struct {
	statusRes

	epoch uint64
}

// Epoch returns creation epoch of the saved object.
func (r *ResObjectPutSingle) Epoch() uint64 {
	return r.epoch
}

func (prm *PrmObjectPutSingle) buildRequest(c *Client) (*v2object.PutSingleRequest, error) {
	if len(prm.XHeaders)%2 != 0 {
		return nil, errorInvalidXHeaders
	}

	body := new(v2object.PutSingleRequestBody)
	body.SetCopiesNumber(prm.CopiesNumber)
	body.SetObject(prm.Object.ToV2())

	meta := new(v2session.RequestMetaHeader)
	writeXHeadersToMeta(prm.XHeaders, meta)

	if prm.BearerToken != nil {
		v2BearerToken := new(acl.BearerToken)
		prm.BearerToken.WriteToV2(v2BearerToken)
		meta.SetBearerToken(v2BearerToken)
	}

	if prm.Session != nil {
		v2SessionToken := new(v2session.Token)
		prm.Session.WriteToV2(v2SessionToken)
		meta.SetSessionToken(v2SessionToken)
	}

	if prm.Local {
		meta.SetTTL(1)
	}

	req := &v2object.PutSingleRequest{}
	req.SetBody(body)
	c.prepareRequest(req, meta)

	return req, nil
}

// ObjectPutSingle writes prepared object to FrostFS.
// Object must have payload, also containerID, objectID, ownerID, payload hash, payload length of an object must be set.
// Exactly one return value is non-nil. By default, server status is returned in res structure.
// Any client's internal or transport errors are returned as Go built-in error.
// If Client is tuned to resolve FrostFS API statuses, then FrostFS failures
// codes are returned as error.
func (c *Client) ObjectPutSingle(ctx context.Context, prm PrmObjectPutSingle) (*ResObjectPutSingle, error) {
	req, err := prm.buildRequest(c)
	if err != nil {
		return nil, err
	}

	key := &c.prm.Key
	if prm.Key != nil {
		key = prm.Key
	}

	err = signature.SignServiceMessage(key, req)
	if err != nil {
		return nil, fmt.Errorf("sign request: %w", err)
	}

	resp, err := rpcapi.PutSingleObject(&c.c, req, client.WithContext(ctx))
	if err != nil {
		return nil, err
	}

	var res ResObjectPutSingle
	res.st, err = c.processResponse(resp)
	if err != nil {
		return nil, err
	}
	res.epoch = resp.GetMetaHeader().GetEpoch()

	return &res, nil
}