package middleware

import (
	"crypto/elliptic"
	"fmt"
	"net/http"
	"strings"

	apiErr "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors"
	"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/logs"
	"git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain"
	"git.frostfs.info/TrueCloudLab/policy-engine/pkg/engine"
	"git.frostfs.info/TrueCloudLab/policy-engine/pkg/resource/testutil"
	"git.frostfs.info/TrueCloudLab/policy-engine/schema/common"
	"git.frostfs.info/TrueCloudLab/policy-engine/schema/s3"
	"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
	"github.com/nspcc-dev/neo-go/pkg/util"
	"go.uber.org/zap"
)

type PolicySettings interface {
	ResolveNamespaceAlias(ns string) string
	PolicyDenyByDefault() bool
}

type FrostFSIDInformer interface {
	GetUserGroupIDs(userHash util.Uint160) ([]string, error)
}

func PolicyCheck(storage engine.ChainRouter, frostfsid FrostFSIDInformer, settings PolicySettings, domains []string, log *zap.Logger) Func {
	return func(h http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			ctx := r.Context()

			st, err := policyCheck(storage, frostfsid, settings, domains, r)
			if err == nil {
				if st != chain.Allow && (st != chain.NoRuleFound || settings.PolicyDenyByDefault()) {
					err = apiErr.GetAPIErrorWithError(apiErr.ErrAccessDenied, fmt.Errorf("policy check: %s", st.String()))
				}
			}
			if err != nil {
				reqLogOrDefault(ctx, log).Error(logs.PolicyValidationFailed, zap.Error(err))
				WriteErrorResponse(w, GetReqInfo(ctx), err)
				return
			}

			h.ServeHTTP(w, r)
		})
	}
}

func policyCheck(storage engine.ChainRouter, frostfsid FrostFSIDInformer, settings PolicySettings, domains []string, r *http.Request) (chain.Status, error) {
	req, err := getPolicyRequest(r, frostfsid, domains)
	if err != nil {
		return 0, err
	}

	reqInfo := GetReqInfo(r.Context())
	target := engine.NewRequestTargetWithNamespace(settings.ResolveNamespaceAlias(reqInfo.Namespace))
	st, found, err := storage.IsAllowed(chain.S3, target, req)
	if err != nil {
		return 0, err
	}

	if !found {
		st = chain.NoRuleFound
	}

	return st, nil
}

func getPolicyRequest(r *http.Request, frostfsid FrostFSIDInformer, domains []string) (*testutil.Request, error) {
	var (
		owner  string
		groups []string
	)

	ctx := r.Context()
	bd, err := GetBoxData(ctx)
	if err == nil && bd.Gate.BearerToken != nil {
		pk, err := keys.NewPublicKeyFromBytes(bd.Gate.BearerToken.SigningKeyBytes(), elliptic.P256())
		if err != nil {
			return nil, fmt.Errorf("parse pubclic key from btoken: %w", err)
		}
		owner = pk.Address()

		groups, err = frostfsid.GetUserGroupIDs(pk.GetScriptHash())
		if err != nil {
			return nil, fmt.Errorf("get group ids: %w", err)
		}
	}

	op, res := determineOperationAndResource(r, domains)

	return testutil.NewRequest(op, testutil.NewResource(res, nil),
		map[string]string{
			s3.PropertyKeyOwner:                owner,
			common.PropertyKeyFrostFSIDGroupID: chain.FormCondSliceContainsValue(groups),
		},
	), nil
}

type ReqType int

const (
	noneType ReqType = iota
	bucketType
	objectType
)

func determineOperationAndResource(r *http.Request, domains []string) (operation string, resource string) {
	var (
		reqType     ReqType
		matchDomain bool
	)

	for _, domain := range domains {
		ind := strings.Index(r.Host, "."+domain)
		if ind == -1 {
			continue
		}

		matchDomain = true
		reqType = bucketType
		bkt := r.Host[:ind]
		if obj := strings.TrimPrefix(r.URL.Path, "/"); obj != "" {
			reqType = objectType
			resource = fmt.Sprintf(s3.ResourceFormatS3BucketObject, bkt, obj)
		} else {
			resource = fmt.Sprintf(s3.ResourceFormatS3Bucket, bkt)
		}

		break
	}

	if !matchDomain {
		bktObj := strings.TrimPrefix(r.URL.Path, "/")
		if ind := strings.IndexByte(bktObj, '/'); ind == -1 {
			reqType = bucketType
			resource = fmt.Sprintf(s3.ResourceFormatS3Bucket, bktObj)
			if bktObj == "" {
				reqType = noneType
			}
		} else {
			reqType = objectType
			resource = fmt.Sprintf(s3.ResourceFormatS3BucketObject, bktObj[:ind], bktObj[ind+1:])
		}
	}

	switch reqType {
	case objectType:
		operation = determineObjectOperation(r)
	case bucketType:
		operation = determineBucketOperation(r)
	default:
		operation = determineGeneralOperation(r)
	}

	return "s3:" + operation, resource
}

func determineBucketOperation(r *http.Request) string {
	query := r.URL.Query()
	switch r.Method {
	case http.MethodOptions:
		return OptionsOperation
	case http.MethodHead:
		return HeadBucketOperation
	case http.MethodGet:
		switch {
		case query.Has(UploadsQuery):
			return ListMultipartUploadsOperation
		case query.Has(LocationQuery):
			return GetBucketLocationOperation
		case query.Has(PolicyQuery):
			return GetBucketPolicyOperation
		case query.Has(LifecycleQuery):
			return GetBucketLifecycleOperation
		case query.Has(EncryptionQuery):
			return GetBucketEncryptionOperation
		case query.Has(CorsQuery):
			return GetBucketCorsOperation
		case query.Has(ACLQuery):
			return GetBucketACLOperation
		case query.Has(WebsiteQuery):
			return GetBucketWebsiteOperation
		case query.Has(AccelerateQuery):
			return GetBucketAccelerateOperation
		case query.Has(RequestPaymentQuery):
			return GetBucketRequestPaymentOperation
		case query.Has(LoggingQuery):
			return GetBucketLoggingOperation
		case query.Has(ReplicationQuery):
			return GetBucketReplicationOperation
		case query.Has(TaggingQuery):
			return GetBucketTaggingOperation
		case query.Has(ObjectLockQuery):
			return GetBucketObjectLockConfigOperation
		case query.Has(VersioningQuery):
			return GetBucketVersioningOperation
		case query.Has(NotificationQuery):
			return GetBucketNotificationOperation
		case query.Has(EventsQuery):
			return ListenBucketNotificationOperation
		case query.Has(VersionsQuery):
			return ListBucketObjectVersionsOperation
		case query.Get(ListTypeQuery) == "2" && query.Get(MetadataQuery) == "true":
			return ListObjectsV2MOperation
		case query.Get(ListTypeQuery) == "2":
			return ListObjectsV2Operation
		default:
			return ListObjectsV1Operation
		}
	case http.MethodPut:
		switch {
		case query.Has(CorsQuery):
			return PutBucketCorsOperation
		case query.Has(ACLQuery):
			return PutBucketACLOperation
		case query.Has(LifecycleQuery):
			return PutBucketLifecycleOperation
		case query.Has(EncryptionQuery):
			return PutBucketEncryptionOperation
		case query.Has(PolicyQuery):
			return PutBucketPolicyOperation
		case query.Has(ObjectLockQuery):
			return PutBucketObjectLockConfigOperation
		case query.Has(TaggingQuery):
			return PutBucketTaggingOperation
		case query.Has(VersioningQuery):
			return PutBucketVersioningOperation
		case query.Has(NotificationQuery):
			return PutBucketNotificationOperation
		default:
			return CreateBucketOperation
		}
	case http.MethodPost:
		switch {
		case query.Has(DeleteQuery):
			return DeleteMultipleObjectsOperation
		default:
			return PostObjectOperation
		}
	case http.MethodDelete:
		switch {
		case query.Has(CorsQuery):
			return DeleteBucketCorsOperation
		case query.Has(WebsiteQuery):
			return DeleteBucketWebsiteOperation
		case query.Has(TaggingQuery):
			return DeleteBucketTaggingOperation
		case query.Has(PolicyQuery):
			return DeleteBucketPolicyOperation
		case query.Has(LifecycleQuery):
			return DeleteBucketLifecycleOperation
		case query.Has(EncryptionQuery):
			return DeleteBucketEncryptionOperation
		default:
			return DeleteBucketOperation
		}
	}

	return ""
}

func determineObjectOperation(r *http.Request) string {
	query := r.URL.Query()
	switch r.Method {
	case http.MethodHead:
		return HeadObjectOperation
	case http.MethodGet:
		switch {
		case query.Has(UploadIDQuery):
			return ListPartsOperation
		case query.Has(ACLQuery):
			return GetObjectACLOperation
		case query.Has(TaggingQuery):
			return GetObjectTaggingOperation
		case query.Has(RetentionQuery):
			return GetObjectRetentionOperation
		case query.Has(LegalQuery):
			return GetObjectLegalHoldOperation
		case query.Has(AttributesQuery):
			return GetObjectAttributesOperation
		default:
			return GetObjectOperation
		}
	case http.MethodPut:
		switch {
		case query.Has(PartNumberQuery) && query.Has(UploadIDQuery) && r.Header.Get("X-Amz-Copy-Source") != "":
			return UploadPartCopyOperation
		case query.Has(PartNumberQuery) && query.Has(UploadIDQuery):
			return UploadPartOperation
		case query.Has(ACLQuery):
			return PutObjectACLOperation
		case query.Has(TaggingQuery):
			return PutObjectTaggingOperation
		case r.Header.Get("X-Amz-Copy-Source") != "":
			return CopyObjectOperation
		case query.Has(RetentionQuery):
			return PutObjectRetentionOperation
		case query.Has(LegalHoldQuery):
			return PutObjectLegalHoldOperation
		default:
			return PutObjectOperation
		}
	case http.MethodPost:
		switch {
		case query.Has(UploadIDQuery):
			return CompleteMultipartUploadOperation
		case query.Has(UploadsQuery):
			return CreateMultipartUploadOperation
		default:
			return SelectObjectContentOperation
		}
	case http.MethodDelete:
		switch {
		case query.Has(UploadIDQuery):
			return AbortMultipartUploadOperation
		case query.Has(TaggingQuery):
			return DeleteObjectTaggingOperation
		default:
			return DeleteObjectOperation
		}
	}

	return ""
}

func determineGeneralOperation(r *http.Request) string {
	if r.Method == http.MethodGet {
		return ListBucketsOperation
	}
	return ""
}