package client

import (
	"context"
	"fmt"

	v2container "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/container"
	"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs"
	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"
	frostfscrypto "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/crypto"
	frostfsecdsa "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/crypto/ecdsa"
	"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/eacl"
	"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/session"
)

// PrmContainerSetEACL groups parameters of ContainerSetEACL operation.
type PrmContainerSetEACL struct {
	prmCommonMeta

	tableSet bool
	table    eacl.Table

	sessionSet bool
	session    session.Container
}

// SetTable sets eACL table structure to be set for the container.
// Required parameter.
func (x *PrmContainerSetEACL) SetTable(table eacl.Table) {
	x.table = table
	x.tableSet = true
}

// WithinSession specifies session within which extended ACL of the container
// should be saved.
//
// Creator of the session acquires the authorship of the request. This affects
// the execution of an operation (e.g. access control).
//
// Session is optional, if set the following requirements apply:
//   - if particular container is specified (ApplyOnlyTo), it MUST equal the container
//     for which extended ACL is going to be set
//   - session operation MUST be session.VerbContainerSetEACL (ForVerb)
//   - token MUST be signed using private key of the owner of the container to be saved
func (x *PrmContainerSetEACL) WithinSession(s session.Container) {
	x.session = s
	x.sessionSet = true
}

func (x *PrmContainerSetEACL) buildRequest(c *Client) (*v2container.SetExtendedACLRequest, error) {
	if !x.tableSet {
		return nil, errorEACLTableNotSet
	}

	eaclV2 := x.table.ToV2()

	var sig frostfscrypto.Signature

	err := sig.Calculate(frostfsecdsa.SignerRFC6979(c.prm.key), eaclV2.StableMarshal(nil))
	if err != nil {
		return nil, fmt.Errorf("calculate signature: %w", err)
	}

	var sigv2 refs.Signature
	sig.WriteToV2(&sigv2)

	reqBody := new(v2container.SetExtendedACLRequestBody)
	reqBody.SetEACL(eaclV2)
	reqBody.SetSignature(&sigv2)

	var meta v2session.RequestMetaHeader
	writeXHeadersToMeta(x.prmCommonMeta.xHeaders, &meta)

	if x.sessionSet {
		var tokv2 v2session.Token
		x.session.WriteToV2(&tokv2)

		meta.SetSessionToken(&tokv2)
	}

	var req v2container.SetExtendedACLRequest
	req.SetBody(reqBody)
	c.prepareRequest(&req, &meta)
	return &req, nil
}

// ResContainerSetEACL groups resulting values of ContainerSetEACL operation.
type ResContainerSetEACL struct {
	statusRes
}

// ContainerSetEACL sends request to update eACL table of the FrostFS container.
//
// 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 `error`.
// If PrmInit.ResolveFrostFSFailures has been called, unsuccessful
// FrostFS status codes are returned as `error`, otherwise, are included
// in the returned result structure.
//
// Operation is asynchronous and no guaranteed even in the absence of errors.
// The required time is also not predictable.
//
// Success can be verified by reading by identifier (see EACL).
//
// Returns an error if parameters are set incorrectly (see PrmContainerSetEACL docs).
// Context is required and must not be nil. It is used for network communication.
//
// Return statuses:
//   - global (see Client docs).
func (c *Client) ContainerSetEACL(ctx context.Context, prm PrmContainerSetEACL) (*ResContainerSetEACL, error) {
	req, err := prm.buildRequest(c)
	if err != nil {
		return nil, err
	}

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

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

	var res ResContainerSetEACL
	res.st, err = c.processResponse(resp)
	return &res, err
}