package middleware

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

	"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data"
	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 {
	PolicyDenyByDefault() bool
	ACLEnabled() bool
}

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

// BucketResolveFunc is a func to resolve bucket info by name.
type BucketResolveFunc func(ctx context.Context, bucket string) (*data.BucketInfo, error)

type PolicyConfig struct {
	Storage        engine.ChainRouter
	FrostfsID      FrostFSIDInformer
	Settings       PolicySettings
	Domains        []string
	Log            *zap.Logger
	BucketResolver BucketResolveFunc
}

func PolicyCheck(cfg PolicyConfig) Func {
	return func(h http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			ctx := r.Context()
			if err := policyCheck(r, cfg); err != nil {
				reqLogOrDefault(ctx, cfg.Log).Error(logs.PolicyValidationFailed, zap.Error(err))
				if _, wrErr := WriteErrorResponse(w, GetReqInfo(ctx), err); wrErr != nil {
					reqLogOrDefault(ctx, cfg.Log).Error(logs.FailedToWriteResponse, zap.Error(wrErr))
				}
				return
			}

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

func policyCheck(r *http.Request, cfg PolicyConfig) error {
	reqType, bktName, objName := getBucketObject(r, cfg.Domains)
	req, userKey, userGroups, err := getPolicyRequest(r, cfg.FrostfsID, reqType, bktName, objName, cfg.Log)
	if err != nil {
		return err
	}

	var bktInfo *data.BucketInfo
	if reqType != noneType && !strings.HasSuffix(req.Operation(), CreateBucketOperation) {
		bktInfo, err = cfg.BucketResolver(r.Context(), bktName)
		if err != nil {
			return err
		}
	}

	reqInfo := GetReqInfo(r.Context())
	target := engine.NewRequestTargetWithNamespace(reqInfo.Namespace)
	if bktInfo != nil {
		cnrTarget := engine.ContainerTarget(bktInfo.CID.EncodeToString())
		target.Container = &cnrTarget
	}

	if userKey != nil {
		entityName := fmt.Sprintf("%s:%s", reqInfo.Namespace, userKey.Address())
		uTarget := engine.UserTarget(entityName)
		target.User = &uTarget
	}

	gts := make([]engine.Target, len(userGroups))
	for i, group := range userGroups {
		entityName := fmt.Sprintf("%s:%s", reqInfo.Namespace, group)
		gts[i] = engine.GroupTarget(entityName)
	}
	target.Groups = gts

	st, found, err := cfg.Storage.IsAllowed(chain.S3, target, req)
	if err != nil {
		return err
	}

	if !found {
		st = chain.NoRuleFound
	}

	switch {
	case st == chain.Allow:
		return nil
	case st != chain.NoRuleFound:
		return apiErr.GetAPIErrorWithError(apiErr.ErrAccessDenied, fmt.Errorf("policy check: %s", st.String()))
	}

	isAPE := !cfg.Settings.ACLEnabled()
	if bktInfo != nil {
		isAPE = bktInfo.APEEnabled
	}

	if isAPE && cfg.Settings.PolicyDenyByDefault() {
		return apiErr.GetAPIErrorWithError(apiErr.ErrAccessDenied, fmt.Errorf("policy check: %s", st.String()))
	}

	return nil
}

func getPolicyRequest(r *http.Request, frostfsid FrostFSIDInformer, reqType ReqType, bktName string, objName string, log *zap.Logger) (*testutil.Request, *keys.PublicKey, []string, error) {
	var (
		owner  string
		groups []string
		pk     *keys.PublicKey
	)

	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, nil, nil, fmt.Errorf("parse pubclic key from btoken: %w", err)
		}
		owner = pk.Address()

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

	op := determineOperation(r, reqType)
	var res string
	switch reqType {
	case objectType:
		res = fmt.Sprintf(s3.ResourceFormatS3BucketObject, bktName, objName)
	default:
		res = fmt.Sprintf(s3.ResourceFormatS3Bucket, bktName)
	}

	reqLogOrDefault(r.Context(), log).Debug(logs.PolicyRequest, zap.String("action", op),
		zap.String("resource", res), zap.String("owner", owner))

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

type ReqType int

const (
	noneType ReqType = iota
	bucketType
	objectType
)

func getBucketObject(r *http.Request, domains []string) (reqType ReqType, bktName string, objName string) {
	for _, domain := range domains {
		ind := strings.Index(r.Host, "."+domain)
		if ind == -1 {
			continue
		}

		bkt := r.Host[:ind]
		if obj := strings.TrimPrefix(r.URL.Path, "/"); obj != "" {
			return objectType, bkt, obj
		}

		return bucketType, bkt, ""
	}

	bktObj := strings.TrimPrefix(r.URL.Path, "/")
	if bktObj == "" {
		return noneType, "", ""
	}

	if ind := strings.IndexByte(bktObj, '/'); ind != -1 && bktObj[ind+1:] != "" {
		return objectType, bktObj[:ind], bktObj[ind+1:]
	}

	return bucketType, strings.TrimSuffix(bktObj, "/"), ""
}

func determineOperation(r *http.Request, reqType ReqType) (operation string) {
	switch reqType {
	case objectType:
		operation = determineObjectOperation(r)
	case bucketType:
		operation = determineBucketOperation(r)
	default:
		operation = determineGeneralOperation(r)
	}

	return "s3:" + operation
}

func determineBucketOperation(r *http.Request) string {
	query := r.URL.Query()
	switch r.Method {
	case http.MethodOptions:
		return OptionsBucketOperation
	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 "UnmatchedBucketOperation"
}

func determineObjectOperation(r *http.Request) string {
	query := r.URL.Query()
	switch r.Method {
	case http.MethodOptions:
		return OptionsObjectOperation
	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 "UnmatchedObjectOperation"
}

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