package ape

import (
	"context"
	"fmt"
	"strconv"

	objectV2 "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/object"
	"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"
	aperesource "git.frostfs.info/TrueCloudLab/policy-engine/pkg/resource"
	nativeschema "git.frostfs.info/TrueCloudLab/policy-engine/schema/native"
)

type request struct {
	operation  string
	resource   *resource
	properties map[string]string
}

var _ aperesource.Request = (*request)(nil)

type resource struct {
	name       string
	properties map[string]string
}

var _ aperesource.Resource = (*resource)(nil)

func (r *resource) Name() string {
	return r.name
}

func (r *resource) Property(key string) string {
	return r.properties[key]
}

func (r *request) Operation() string {
	return r.operation
}

func (r *request) Property(key string) string {
	return r.properties[key]
}

func (r *request) Resource() aperesource.Resource {
	return r.resource
}

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, header *objectV2.Header) map[string]string {
	objectProps := map[string]string{
		nativeschema.PropertyKeyObjectContainerID: cnr.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) (*request, error) {
	switch prm.Method {
	case nativeschema.MethodGetObject,
		nativeschema.MethodHeadObject,
		nativeschema.MethodRangeObject,
		nativeschema.MethodHashObject,
		nativeschema.MethodDeleteObject:
		if prm.Object == nil {
			return nil, fmt.Errorf("method %s: %w", prm.Method, errMissingOID)
		}
	case nativeschema.MethodSearchObject, nativeschema.MethodPutObject:
	default:
		return nil, 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)
		if err == nil {
			header = headerObjSDK.ToV2().GetHeader()
		}
	}

	return &request{
		operation: prm.Method,
		resource: &resource{
			name:       resourceName(prm.Container, prm.Object, prm.Namespace),
			properties: objectProperties(prm.Container, prm.Object, header),
		},
		properties: map[string]string{
			nativeschema.PropertyKeyActorPublicKey: prm.SenderKey,
			nativeschema.PropertyKeyActorRole:      prm.Role,
		},
	}, nil
}