package handler

import (
	"encoding/xml"
	"errors"
	"fmt"
	"io"
	"strconv"
	"strings"
	"time"

	"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api"
	"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/layer"
	"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/logs"
	cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
	"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/netmap"
	"git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain"
	"go.uber.org/zap"
)

type (
	handler struct {
		log         *zap.Logger
		obj         layer.Client
		notificator Notificator
		cfg         Config
		ape         APE
		frostfsid   FrostFSID
	}

	Notificator interface {
		SendNotifications(topics map[string]string, p *SendNotificationParams) error
		SendTestNotification(topic, bucketName, requestID, HostID string, now time.Time) error
	}

	// Config contains data which handler needs to keep.
	Config interface {
		DefaultPlacementPolicy(namespace string) netmap.PlacementPolicy
		PlacementPolicy(namespace, constraint string) (netmap.PlacementPolicy, bool)
		CopiesNumbers(namespace, constraint string) ([]uint32, bool)
		DefaultCopiesNumbers(namespace string) []uint32
		NewXMLDecoder(io.Reader) *xml.Decoder
		DefaultMaxAge() int
		NotificatorEnabled() bool
		ResolveZoneList() []string
		IsResolveListAllow() bool
		BypassContentEncodingInChunks() bool
		MD5Enabled() bool
		ACLEnabled() bool
	}

	FrostFSID interface {
		GetUserAddress(account, user string) (string, error)
		GetUserKey(account, name string) (string, error)
	}

	// APE is Access Policy Engine that needs to save policy and acl info to different places.
	APE interface {
		PutBucketPolicy(ns string, cnrID cid.ID, policy []byte, chains []*chain.Chain) error
		DeleteBucketPolicy(ns string, cnrID cid.ID, chainIDs []chain.ID) error
		GetBucketPolicy(ns string, cnrID cid.ID) ([]byte, error)
		SaveACLChains(cid string, chains []*chain.Chain) error
	}
)

var _ api.Handler = (*handler)(nil)

// New creates new api.Handler using given logger and client.
func New(log *zap.Logger, obj layer.Client, notificator Notificator, cfg Config, storage APE, ffsid FrostFSID) (api.Handler, error) {
	switch {
	case obj == nil:
		return nil, errors.New("empty FrostFS Object Layer")
	case log == nil:
		return nil, errors.New("empty logger")
	case storage == nil:
		return nil, errors.New("empty policy storage")
	case ffsid == nil:
		return nil, errors.New("empty frostfsid")
	}

	if !cfg.NotificatorEnabled() {
		log.Warn(logs.NotificatorIsDisabledS3WontProduceNotificationEvents)
	} else if notificator == nil {
		return nil, errors.New("empty notificator")
	}

	return &handler{
		log:         log,
		obj:         obj,
		cfg:         cfg,
		ape:         storage,
		notificator: notificator,
		frostfsid:   ffsid,
	}, nil
}

// pickCopiesNumbers chooses the return values following this logic:
// 1) array of copies numbers sent in request's header has the highest priority.
// 2) array of copies numbers with corresponding location constraint provided in the config file.
// 3) default copies number from the config file wrapped into array.
func (h *handler) pickCopiesNumbers(metadata map[string]string, namespace, locationConstraint string) ([]uint32, error) {
	copiesNumbersStr, ok := metadata[layer.AttributeFrostfsCopiesNumber]
	if ok {
		result, err := parseCopiesNumbers(copiesNumbersStr)
		if err != nil {
			return nil, err
		}
		return result, nil
	}

	copiesNumbers, ok := h.cfg.CopiesNumbers(namespace, locationConstraint)
	if ok {
		return copiesNumbers, nil
	}

	return h.cfg.DefaultCopiesNumbers(namespace), nil
}

func parseCopiesNumbers(copiesNumbersStr string) ([]uint32, error) {
	var result []uint32
	copiesNumbersSplit := strings.Split(copiesNumbersStr, ",")

	for i := range copiesNumbersSplit {
		item := strings.ReplaceAll(copiesNumbersSplit[i], " ", "")
		if len(item) == 0 {
			continue
		}
		copiesNumber, err := strconv.ParseUint(item, 10, 32)
		if err != nil {
			return nil, fmt.Errorf("pasrse copies number: %w", err)
		}
		result = append(result, uint32(copiesNumber))
	}

	return result, nil
}