package handler

import (
	"context"
	"crypto/elliptic"
	"encoding/hex"
	"encoding/json"
	stderrors "errors"
	"fmt"
	"io"
	"net/http"
	"strings"

	"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api"
	"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data"
	"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors"
	"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/layer"
	"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/middleware"
	"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/creds/accessbox"
	"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/logs"
	engineiam "git.frostfs.info/TrueCloudLab/policy-engine/iam"
	"git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain"
	"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
	"go.uber.org/zap"
)

const (
	arnAwsPrefix  = "arn:aws:s3:::"
	allUsersGroup = "http://acs.amazonaws.com/groups/global/AllUsers"
)

// AWSACL is aws permission constants.
type AWSACL string

const (
	aclFullControl AWSACL = "FULL_CONTROL"
	aclWrite       AWSACL = "WRITE"
	aclRead        AWSACL = "READ"
)

// GranteeType is aws grantee permission type constants.
type GranteeType string

const (
	acpCanonicalUser GranteeType = "CanonicalUser"
	acpGroup         GranteeType = "Group"
)

func (h *handler) GetBucketACLHandler(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()
	reqInfo := middleware.GetReqInfo(ctx)

	bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName)
	if err != nil {
		h.logAndSendError(ctx, w, "could not get bucket info", reqInfo, err)
		return
	}

	settings, err := h.obj.GetBucketSettings(ctx, bktInfo)
	if err != nil {
		h.logAndSendError(ctx, w, "couldn't get bucket settings", reqInfo, err)
		return
	}

	if err = middleware.EncodeToResponse(w, h.encodeBucketCannedACL(ctx, bktInfo, settings)); err != nil {
		h.logAndSendError(ctx, w, "something went wrong", reqInfo, err)
		return
	}
}

func (h *handler) encodeBucketCannedACL(ctx context.Context, bktInfo *data.BucketInfo, settings *data.BucketSettings) *AccessControlPolicy {
	res := h.encodePrivateCannedACL(ctx, bktInfo, settings)

	switch settings.CannedACL {
	case basicACLPublic:
		grantee := NewGrantee(acpGroup)
		grantee.URI = allUsersGroup

		res.AccessControlList = append(res.AccessControlList, &Grant{
			Grantee:    grantee,
			Permission: aclWrite,
		})
		fallthrough
	case basicACLReadOnly:
		grantee := NewGrantee(acpGroup)
		grantee.URI = allUsersGroup

		res.AccessControlList = append(res.AccessControlList, &Grant{
			Grantee:    grantee,
			Permission: aclRead,
		})
	}

	return res
}

func (h *handler) encodePrivateCannedACL(ctx context.Context, bktInfo *data.BucketInfo, settings *data.BucketSettings) *AccessControlPolicy {
	ownerDisplayName := bktInfo.Owner.EncodeToString()
	ownerEncodedID := ownerDisplayName

	if settings.OwnerKey == nil {
		h.reqLogger(ctx).Warn(logs.BucketOwnerKeyIsMissing, zap.String("owner", bktInfo.Owner.String()))
	} else {
		ownerDisplayName = settings.OwnerKey.Address()
		ownerEncodedID = hex.EncodeToString(settings.OwnerKey.Bytes())
	}

	res := &AccessControlPolicy{Owner: Owner{
		ID:          ownerEncodedID,
		DisplayName: ownerDisplayName,
	}}

	return res
}

func getTokenIssuerKey(box *accessbox.Box) (*keys.PublicKey, error) {
	if box.Gate.BearerToken == nil {
		return nil, stderrors.New("bearer token is missing")
	}

	key, err := keys.NewPublicKeyFromBytes(box.Gate.BearerToken.SigningKeyBytes(), elliptic.P256())
	if err != nil {
		return nil, fmt.Errorf("public key from bytes: %w", err)
	}

	return key, nil
}

func (h *handler) PutBucketACLHandler(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()
	reqInfo := middleware.GetReqInfo(ctx)

	bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName)
	if err != nil {
		h.logAndSendError(ctx, w, "could not get bucket info", reqInfo, err)
		return
	}

	settings, err := h.obj.GetBucketSettings(ctx, bktInfo)
	if err != nil {
		h.logAndSendError(ctx, w, "couldn't get bucket settings", reqInfo, err)
		return
	}

	h.putBucketACLAPEHandler(w, r, reqInfo, bktInfo, settings)
}

func (h *handler) putBucketACLAPEHandler(w http.ResponseWriter, r *http.Request, reqInfo *middleware.ReqInfo, bktInfo *data.BucketInfo, settings *data.BucketSettings) {
	ctx := r.Context()

	defer func() {
		if errBody := r.Body.Close(); errBody != nil {
			h.reqLogger(ctx).Warn(logs.CouldNotCloseRequestBody, zap.Error(errBody))
		}
	}()

	written, err := io.Copy(io.Discard, r.Body)
	if err != nil {
		h.logAndSendError(ctx, w, "couldn't read request body", reqInfo, err)
		return
	}

	if written != 0 || len(r.Header.Get(api.AmzACL)) == 0 {
		h.logAndSendError(ctx, w, "acl not supported for this bucket", reqInfo, errors.GetAPIError(errors.ErrAccessControlListNotSupported))
		return
	}

	cannedACL, err := parseCannedACL(r.Header)
	if err != nil {
		h.logAndSendError(ctx, w, "could not parse canned ACL", reqInfo, err)
		return
	}

	chainRules := bucketCannedACLToAPERules(cannedACL, reqInfo, bktInfo.CID)
	if err = h.ape.SaveACLChains(bktInfo.CID.EncodeToString(), chainRules); err != nil {
		h.logAndSendError(ctx, w, "failed to add morph rule chains", reqInfo, err)
		return
	}

	settings.CannedACL = cannedACL

	sp := &layer.PutSettingsParams{
		BktInfo:  bktInfo,
		Settings: settings,
	}

	if err = h.obj.PutBucketSettings(ctx, sp); err != nil {
		h.logAndSendError(ctx, w, "couldn't save bucket settings", reqInfo, err,
			zap.String("container_id", bktInfo.CID.EncodeToString()))
		return
	}

	w.WriteHeader(http.StatusOK)
}

func (h *handler) GetObjectACLHandler(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()
	reqInfo := middleware.GetReqInfo(ctx)

	bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName)
	if err != nil {
		h.logAndSendError(ctx, w, "could not get bucket info", reqInfo, err)
		return
	}

	settings, err := h.obj.GetBucketSettings(ctx, bktInfo)
	if err != nil {
		h.logAndSendError(ctx, w, "couldn't get bucket settings", reqInfo, err)
		return
	}

	if err = middleware.EncodeToResponse(w, h.encodePrivateCannedACL(ctx, bktInfo, settings)); err != nil {
		h.logAndSendError(ctx, w, "something went wrong", reqInfo, err)
		return
	}
}

func (h *handler) PutObjectACLHandler(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()
	reqInfo := middleware.GetReqInfo(ctx)

	if _, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName); err != nil {
		h.logAndSendError(ctx, w, "could not get bucket info", reqInfo, err)
		return
	}

	h.logAndSendError(ctx, w, "acl not supported for this bucket", reqInfo, errors.GetAPIError(errors.ErrAccessControlListNotSupported))
}

func (h *handler) GetBucketPolicyStatusHandler(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()
	reqInfo := middleware.GetReqInfo(ctx)

	bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName)
	if err != nil {
		h.logAndSendError(ctx, w, "could not get bucket info", reqInfo, err)
		return
	}

	jsonPolicy, err := h.ape.GetBucketPolicy(reqInfo.Namespace, bktInfo.CID)
	if err != nil {
		if strings.Contains(err.Error(), "not found") {
			err = fmt.Errorf("%w: %s", errors.GetAPIError(errors.ErrNoSuchBucketPolicy), err.Error())
		}
		h.logAndSendError(ctx, w, "failed to get policy from storage", reqInfo, err)
		return
	}

	var bktPolicy engineiam.Policy
	if err = json.Unmarshal(jsonPolicy, &bktPolicy); err != nil {
		h.logAndSendError(ctx, w, "could not parse bucket policy", reqInfo, err)
		return
	}

	policyStatus := &PolicyStatus{
		IsPublic: PolicyStatusIsPublicFalse,
	}

	for _, st := range bktPolicy.Statement {
		// https://docs.aws.amazon.com/AmazonS3/latest/userguide/access-control-block-public-access.html#access-control-block-public-access-policy-status
		if _, ok := st.Principal[engineiam.Wildcard]; ok {
			policyStatus.IsPublic = PolicyStatusIsPublicTrue
			break
		}
	}

	if err = middleware.EncodeToResponse(w, policyStatus); err != nil {
		h.logAndSendError(ctx, w, "encode and write response", reqInfo, err)
		return
	}
}

func (h *handler) GetBucketPolicyHandler(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()
	reqInfo := middleware.GetReqInfo(ctx)

	bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName)
	if err != nil {
		h.logAndSendError(ctx, w, "could not get bucket info", reqInfo, err)
		return
	}

	jsonPolicy, err := h.ape.GetBucketPolicy(reqInfo.Namespace, bktInfo.CID)
	if err != nil {
		if strings.Contains(err.Error(), "not found") {
			err = fmt.Errorf("%w: %s", errors.GetAPIError(errors.ErrNoSuchBucketPolicy), err.Error())
		}
		h.logAndSendError(ctx, w, "failed to get policy from storage", reqInfo, err)
		return
	}

	w.Header().Set(api.ContentType, "application/json")
	w.WriteHeader(http.StatusOK)

	if _, err = w.Write(jsonPolicy); err != nil {
		h.logAndSendError(ctx, w, "write json policy to client", reqInfo, err)
	}
}

func (h *handler) DeleteBucketPolicyHandler(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()
	reqInfo := middleware.GetReqInfo(ctx)

	bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName)
	if err != nil {
		h.logAndSendError(ctx, w, "could not get bucket info", reqInfo, err)
		return
	}

	chainIDs := []chain.ID{getBucketChainID(chain.S3, bktInfo), getBucketChainID(chain.Ingress, bktInfo)}
	if err = h.ape.DeleteBucketPolicy(reqInfo.Namespace, bktInfo.CID, chainIDs); err != nil {
		h.logAndSendError(ctx, w, "failed to delete policy from storage", reqInfo, err)
		return
	}
}

func checkOwner(info *data.BucketInfo, owner string) error {
	if owner == "" {
		return nil
	}

	// may need to convert owner to appropriate format
	if info.Owner.String() != owner {
		return fmt.Errorf("%w: mismatch owner", errors.GetAPIError(errors.ErrAccessDenied))
	}

	return nil
}

func (h *handler) PutBucketPolicyHandler(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()
	reqInfo := middleware.GetReqInfo(ctx)

	bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName)
	if err != nil {
		h.logAndSendError(ctx, w, "could not get bucket info", reqInfo, err)
		return
	}

	jsonPolicy, err := io.ReadAll(r.Body)
	if err != nil {
		h.logAndSendError(ctx, w, "read body", reqInfo, err)
		return
	}

	var bktPolicy engineiam.Policy
	if err = json.Unmarshal(jsonPolicy, &bktPolicy); err != nil {
		h.logAndSendError(ctx, w, "could not parse bucket policy", reqInfo, err)
		return
	}

	for _, stat := range bktPolicy.Statement {
		if len(stat.NotResource) != 0 {
			h.logAndSendError(ctx, w, "policy resource mismatched bucket", reqInfo, errors.GetAPIError(errors.ErrMalformedPolicy))
			return
		}

		if len(stat.NotPrincipal) != 0 && stat.Effect == engineiam.AllowEffect {
			h.logAndSendError(ctx, w, "invalid NotPrincipal", reqInfo, errors.GetAPIError(errors.ErrMalformedPolicyNotPrincipal))
			return
		}

		for _, resource := range stat.Resource {
			if reqInfo.BucketName != strings.Split(strings.TrimPrefix(resource, arnAwsPrefix), "/")[0] {
				h.logAndSendError(ctx, w, "policy resource mismatched bucket", reqInfo, errors.GetAPIError(errors.ErrMalformedPolicy))
				return
			}
		}
	}

	s3Chain, err := engineiam.ConvertToS3Chain(bktPolicy, h.frostfsid)
	if err != nil {
		h.logAndSendError(ctx, w, "could not convert s3 policy to chain policy", reqInfo, err)
		return
	}
	s3Chain.ID = getBucketChainID(chain.S3, bktInfo)

	nativeChain, err := engineiam.ConvertToNativeChain(bktPolicy, h.nativeResolver(reqInfo.Namespace, bktInfo))
	if err == nil {
		nativeChain.ID = getBucketChainID(chain.Ingress, bktInfo)
	} else if !stderrors.Is(err, engineiam.ErrActionsNotApplicable) {
		h.logAndSendError(ctx, w, "could not convert s3 policy to native chain policy", reqInfo, err)
		return
	} else {
		h.reqLogger(ctx).Warn(logs.PolicyCouldntBeConvertedToNativeRules)
	}

	chainsToSave := []*chain.Chain{s3Chain}
	if nativeChain != nil {
		chainsToSave = append(chainsToSave, nativeChain)
	}

	if err = h.ape.PutBucketPolicy(reqInfo.Namespace, bktInfo.CID, jsonPolicy, chainsToSave); err != nil {
		h.logAndSendError(ctx, w, "failed to update policy in contract", reqInfo, err)
		return
	}
}

type nativeResolver struct {
	FrostFSID
	namespace string
	bktInfo   *data.BucketInfo
}

func (n *nativeResolver) GetBucketInfo(bucket string) (*engineiam.BucketInfo, error) {
	if n.bktInfo.Name != bucket {
		return nil, fmt.Errorf("invalid bucket %s: %w", bucket, errors.GetAPIError(errors.ErrMalformedPolicy))
	}

	return &engineiam.BucketInfo{Namespace: n.namespace, Container: n.bktInfo.CID.EncodeToString()}, nil
}

func (h *handler) nativeResolver(ns string, bktInfo *data.BucketInfo) engineiam.NativeResolver {
	return &nativeResolver{
		FrostFSID: h.frostfsid,
		namespace: ns,
		bktInfo:   bktInfo,
	}
}

func getBucketChainID(prefix chain.Name, bktInfo *data.BucketInfo) chain.ID {
	return chain.ID(string(prefix) + ":bkt" + string(bktInfo.CID[:]))
}