package ape

import (
	"context"
	"crypto/sha256"
	"errors"
	"fmt"
	"net"
	"strconv"

	aperequest "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/ape/request"
	"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/core/netmap"
	"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/core/object"
	objectV2 "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/api/object"
	apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status"
	"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/acl"
	cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
	objectSDK "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"
	commonschema "git.frostfs.info/TrueCloudLab/policy-engine/schema/common"
	nativeschema "git.frostfs.info/TrueCloudLab/policy-engine/schema/native"
	"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
	"google.golang.org/grpc/peer"
)

var defaultRequest = aperequest.Request{}

var errECMissingParentObjectID = errors.New("missing EC parent object ID")

func nativeSchemaRole(role acl.Role) string {
	switch role {
	case acl.RoleOwner:
		return nativeschema.PropertyValueContainerRoleOwner
	case acl.RoleContainer:
		return nativeschema.PropertyValueContainerRoleContainer
	case acl.RoleInnerRing:
		return nativeschema.PropertyValueContainerRoleIR
	case acl.RoleOthers:
		return nativeschema.PropertyValueContainerRoleOthers
	default:
		return ""
	}
}

func resourceName(cid cid.ID, oid *oid.ID, namespace string) string {
	if namespace == "root" || namespace == "" {
		if oid != nil {
			return fmt.Sprintf(nativeschema.ResourceFormatRootContainerObject, cid.EncodeToString(), oid.EncodeToString())
		}
		return fmt.Sprintf(nativeschema.ResourceFormatRootContainerObjects, cid.EncodeToString())
	}
	if oid != nil {
		return fmt.Sprintf(nativeschema.ResourceFormatNamespaceContainerObject, namespace, cid.EncodeToString(), oid.EncodeToString())
	}
	return fmt.Sprintf(nativeschema.ResourceFormatNamespaceContainerObjects, namespace, cid.EncodeToString())
}

// objectProperties collects object properties from address parameters and a header if it is passed.
func objectProperties(cnr cid.ID, oid *oid.ID, cnrOwner user.ID, header *objectV2.Header) map[string]string {
	objectProps := map[string]string{
		nativeschema.PropertyKeyObjectContainerID: cnr.EncodeToString(),
	}

	objectProps[nativeschema.PropertyKeyContainerOwnerID] = cnrOwner.EncodeToString()

	if oid != nil {
		objectProps[nativeschema.PropertyKeyObjectID] = oid.String()
	}

	if header == nil {
		return objectProps
	}

	objV2 := new(objectV2.Object)
	objV2.SetHeader(header)
	objSDK := objectSDK.NewFromV2(objV2)

	objectProps[nativeschema.PropertyKeyObjectVersion] = objSDK.Version().String()
	objectProps[nativeschema.PropertyKeyObjectOwnerID] = objSDK.OwnerID().EncodeToString()
	objectProps[nativeschema.PropertyKeyObjectCreationEpoch] = strconv.Itoa(int(objSDK.CreationEpoch()))
	objectProps[nativeschema.PropertyKeyObjectPayloadLength] = strconv.Itoa(int(objSDK.PayloadSize()))
	objectProps[nativeschema.PropertyKeyObjectType] = objSDK.Type().String()

	pcs, isSet := objSDK.PayloadChecksum()
	if isSet {
		objectProps[nativeschema.PropertyKeyObjectPayloadHash] = pcs.String()
	}
	hcs, isSet := objSDK.PayloadHomomorphicHash()
	if isSet {
		objectProps[nativeschema.PropertyKeyObjectHomomorphicHash] = hcs.String()
	}

	for _, attr := range header.GetAttributes() {
		objectProps[attr.GetKey()] = attr.GetValue()
	}

	return objectProps
}

// newAPERequest creates an APE request to be passed to a chain router. It collects resource properties from
// header provided by headerProvider. If it cannot be found in headerProvider, then properties are
// initialized from header given in prm (if it is set). Otherwise, just CID and OID are set to properties.
func (c *checkerImpl) newAPERequest(ctx context.Context, prm Prm) (aperequest.Request, error) {
	switch prm.Method {
	case nativeschema.MethodGetObject,
		nativeschema.MethodHeadObject,
		nativeschema.MethodRangeObject,
		nativeschema.MethodHashObject,
		nativeschema.MethodDeleteObject,
		nativeschema.MethodPatchObject:
		if prm.Object == nil {
			return defaultRequest, fmt.Errorf("method %s: %w", prm.Method, errMissingOID)
		}
	case nativeschema.MethodSearchObject, nativeschema.MethodPutObject:
	default:
		return defaultRequest, fmt.Errorf("unknown method: %s", prm.Method)
	}

	var header *objectV2.Header
	if prm.Header != nil {
		header = prm.Header
	} else if prm.Object != nil {
		headerObjSDK, err := c.headerProvider.GetHeader(ctx, prm.Container, *prm.Object, true)
		if err == nil {
			header = headerObjSDK.ToV2().GetHeader()
		}
	}
	header, err := c.fillHeaderWithECParent(ctx, prm, header)
	if err != nil {
		return defaultRequest, fmt.Errorf("get EC parent header: %w", err)
	}
	reqProps := map[string]string{
		nativeschema.PropertyKeyActorPublicKey: prm.SenderKey,
		nativeschema.PropertyKeyActorRole:      prm.Role,
	}

	for _, xhead := range prm.XHeaders {
		xheadKey := fmt.Sprintf(commonschema.PropertyKeyFrostFSXHeader, xhead.GetKey())
		reqProps[xheadKey] = xhead.GetValue()
	}

	reqProps, err = c.fillWithUserClaimTags(reqProps, prm)
	if err != nil {
		return defaultRequest, err
	}

	if p, ok := peer.FromContext(ctx); ok {
		if tcpAddr, ok := p.Addr.(*net.TCPAddr); ok {
			reqProps[commonschema.PropertyKeyFrostFSSourceIP] = tcpAddr.IP.String()
		}
	}

	return aperequest.NewRequest(
		prm.Method,
		aperequest.NewResource(
			resourceName(prm.Container, prm.Object, prm.Namespace),
			objectProperties(prm.Container, prm.Object, prm.ContainerOwner, header),
		),
		reqProps,
	), nil
}

func (c *checkerImpl) fillHeaderWithECParent(ctx context.Context, prm Prm, header *objectV2.Header) (*objectV2.Header, error) {
	if header == nil {
		return header, nil
	}
	if header.GetEC() == nil {
		return header, nil
	}
	parentObjRefID := header.GetEC().Parent
	if parentObjRefID == nil {
		return nil, errECMissingParentObjectID
	}
	var parentObjID oid.ID
	if err := parentObjID.ReadFromV2(*parentObjRefID); err != nil {
		return nil, fmt.Errorf("EC parent object ID format error: %w", err)
	}
	// only container node have access to collect parent object
	contNode, err := c.currentNodeIsContainerNode(prm.Container)
	if err != nil {
		return nil, fmt.Errorf("check container node status: %w", err)
	}
	if !contNode {
		return header, nil
	}
	parentObj, err := c.headerProvider.GetHeader(ctx, prm.Container, parentObjID, false)
	if err != nil {
		if isLogicalError(err) {
			return header, nil
		}
		return nil, fmt.Errorf("EC parent header request: %w", err)
	}
	return parentObj.ToV2().GetHeader(), nil
}

func isLogicalError(err error) bool {
	var errObjRemoved *apistatus.ObjectAlreadyRemoved
	var errObjNotFound *apistatus.ObjectNotFound
	return errors.As(err, &errObjRemoved) || errors.As(err, &errObjNotFound)
}

func (c *checkerImpl) currentNodeIsContainerNode(cnrID cid.ID) (bool, error) {
	cnr, err := c.cnrSource.Get(cnrID)
	if err != nil {
		return false, err
	}

	nm, err := netmap.GetLatestNetworkMap(c.nm)
	if err != nil {
		return false, err
	}
	idCnr := make([]byte, sha256.Size)
	cnrID.Encode(idCnr)

	in, err := object.LookupKeyInContainer(nm, c.nodePK, idCnr, cnr.Value)
	if err != nil {
		return false, err
	} else if in {
		return true, nil
	}

	nm, err = netmap.GetPreviousNetworkMap(c.nm)
	if err != nil {
		return false, err
	}

	return object.LookupKeyInContainer(nm, c.nodePK, idCnr, cnr.Value)
}

// fillWithUserClaimTags fills ape request properties with user claim tags getting them from frostfsid contract by actor public key.
func (c *checkerImpl) fillWithUserClaimTags(reqProps map[string]string, prm Prm) (map[string]string, error) {
	if reqProps == nil {
		reqProps = make(map[string]string)
	}
	pk, err := keys.NewPublicKeyFromString(prm.SenderKey)
	if err != nil {
		return nil, err
	}
	props, err := aperequest.FormFrostfsIDRequestProperties(c.frostFSIDClient, pk)
	if err != nil {
		return reqProps, err
	}
	for propertyName, properyValue := range props {
		reqProps[propertyName] = properyValue
	}
	return reqProps, nil
}