package session

import (
	"crypto/ecdsa"
	"errors"
	"fmt"

	"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs"
	"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/session"
	cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
	frostfscrypto "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/crypto"
	"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user"
)

// Container represents token of the FrostFS Container session. A session is opened
// between any two sides of the system, and implements a mechanism for transferring
// the power of attorney of actions to another network member. The session has a
// limited validity period, and applies to a strictly defined set of operations.
// See methods for details.
//
// Container is mutually compatible with git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/session.Token
// message. See ReadFromV2 / WriteToV2 methods.
//
// Instances can be created using built-in var declaration.
type Container struct {
	commonData

	verb ContainerVerb

	cnrSet bool
	cnr    cid.ID
}

// readContext is a contextReader needed for commonData methods.
func (x *Container) readContext(c session.TokenContext, checkFieldPresence bool) error {
	cCnr, ok := c.(*session.ContainerSessionContext)
	if !ok || cCnr == nil {
		return fmt.Errorf("invalid context %T", c)
	}

	x.cnrSet = !cCnr.Wildcard()
	cnr := cCnr.ContainerID()

	if x.cnrSet {
		if cnr != nil {
			err := x.cnr.ReadFromV2(*cnr)
			if err != nil {
				return fmt.Errorf("invalid container ID: %w", err)
			}
		} else if checkFieldPresence {
			return errors.New("missing container or wildcard flag")
		}
	} else if cnr != nil {
		return errors.New("container conflicts with wildcard flag")
	}

	x.verb = ContainerVerb(cCnr.Verb())

	return nil
}

func (x *Container) readFromV2(m session.Token, checkFieldPresence bool) error {
	return x.commonData.readFromV2(m, checkFieldPresence, x.readContext)
}

// ReadFromV2 reads Container from the session.Token message. Checks if the
// message conforms to FrostFS API V2 protocol.
//
// See also WriteToV2.
func (x *Container) ReadFromV2(m session.Token) error {
	return x.readFromV2(m, true)
}

func (x Container) writeContext() session.TokenContext {
	var c session.ContainerSessionContext
	c.SetWildcard(!x.cnrSet)
	c.SetVerb(session.ContainerSessionVerb(x.verb))

	if x.cnrSet {
		var cnr refs.ContainerID
		x.cnr.WriteToV2(&cnr)

		c.SetContainerID(&cnr)
	}

	return &c
}

// WriteToV2 writes Container to the session.Token message.
// The message must not be nil.
//
// See also ReadFromV2.
func (x Container) WriteToV2(m *session.Token) {
	x.writeToV2(m, x.writeContext)
}

// Marshal encodes Container into a binary format of the FrostFS API protocol
// (Protocol Buffers with direct field order).
//
// See also Unmarshal.
func (x Container) Marshal() []byte {
	return x.marshal(x.writeContext)
}

// Unmarshal decodes FrostFS API protocol binary format into the Container
// (Protocol Buffers with direct field order). Returns an error describing
// a format violation.
//
// See also Marshal.
func (x *Container) Unmarshal(data []byte) error {
	return x.unmarshal(data, x.readContext)
}

// MarshalJSON encodes Container into a JSON format of the FrostFS API protocol
// (Protocol Buffers JSON).
//
// See also UnmarshalJSON.
func (x Container) MarshalJSON() ([]byte, error) {
	return x.marshalJSON(x.writeContext)
}

// UnmarshalJSON decodes FrostFS API protocol JSON format into the Container
// (Protocol Buffers JSON). Returns an error describing a format violation.
//
// See also MarshalJSON.
func (x *Container) UnmarshalJSON(data []byte) error {
	return x.unmarshalJSON(data, x.readContext)
}

// Sign calculates and writes signature of the Container data.
// Returns signature calculation errors.
//
// Zero Container is unsigned.
//
// Note that any Container mutation is likely to break the signature, so it is
// expected to be calculated as a final stage of Container formation.
//
// See also VerifySignature.
func (x *Container) Sign(key ecdsa.PrivateKey) error {
	return x.sign(key, x.writeContext)
}

// VerifySignature checks if Container signature is presented and valid.
//
// Zero Container fails the check.
//
// See also Sign.
func (x Container) VerifySignature() bool {
	return x.verifySignature(x.writeContext)
}

// ApplyOnlyTo limits session scope to a given author container.
//
// See also AppliedTo.
func (x *Container) ApplyOnlyTo(cnr cid.ID) {
	x.cnr = cnr
	x.cnrSet = true
}

// AppliedTo checks if the session is propagated to the given container.
//
// Zero Container is applied to all author's containers.
//
// See also ApplyOnlyTo.
func (x Container) AppliedTo(cnr cid.ID) bool {
	return !x.cnrSet || x.cnr.Equals(cnr)
}

// ContainerVerb enumerates container operations.
type ContainerVerb int8

const (
	_ ContainerVerb = iota

	VerbContainerPut     // Put rpc
	VerbContainerDelete  // Delete rpc
	VerbContainerSetEACL // SetExtendedACL rpc
)

// ForVerb specifies the container operation of the session scope. Each
// Container is related to the single operation.
//
// See also AssertVerb.
func (x *Container) ForVerb(verb ContainerVerb) {
	x.verb = verb
}

// AssertVerb checks if Container relates to the given container operation.
//
// Zero Container relates to zero (unspecified) verb.
//
// See also ForVerb.
func (x Container) AssertVerb(verb ContainerVerb) bool {
	return x.verb == verb
}

// IssuedBy checks if Container session is issued by the given user.
//
// See also Container.Issuer.
func IssuedBy(cnr Container, id user.ID) bool {
	return cnr.Issuer().Equals(id)
}

// VerifySessionDataSignature verifies signature of the session data. In practice,
// the method is used to authenticate an operation with session data.
func (x Container) VerifySessionDataSignature(data, signature []byte) bool {
	var sigV2 refs.Signature
	sigV2.SetKey(x.authKey)
	sigV2.SetScheme(refs.ECDSA_RFC6979_SHA256)
	sigV2.SetSign(signature)

	var sig frostfscrypto.Signature

	return sig.ReadFromV2(sigV2) == nil && sig.Verify(data)
}