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"
	"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/netmap"
	"go.uber.org/zap"
)

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

	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 struct {
		Policy                     PlacementPolicy
		XMLDecoder                 XMLDecoderProvider
		DefaultMaxAge              int
		NotificatorEnabled         bool
		ResolveZoneList            []string
		IsResolveListAllow         bool // True if ResolveZoneList contains allowed zones
		CompleteMultipartKeepalive time.Duration
		Kludge                     KludgeSettings
	}

	PlacementPolicy interface {
		DefaultPlacementPolicy() netmap.PlacementPolicy
		PlacementPolicy(string) (netmap.PlacementPolicy, bool)
		CopiesNumbers(string) ([]uint32, bool)
		DefaultCopiesNumbers() []uint32
	}

	XMLDecoderProvider interface {
		NewCompleteMultipartDecoder(io.Reader) *xml.Decoder
	}

	KludgeSettings interface {
		BypassContentEncodingInChunks() bool
	}
)

const (
	// DefaultPolicy is a default policy of placing containers in FrostFS if it's not set at the request.
	DefaultPolicy = "REP 3"
	// DefaultCopiesNumber is a default number of object copies that is enough to consider put successful if it's not set in config.
	DefaultCopiesNumber uint32 = 0
)

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) (api.Handler, error) {
	switch {
	case obj == nil:
		return nil, errors.New("empty FrostFS Object Layer")
	case log == nil:
		return nil, errors.New("empty logger")
	}

	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,
		notificator: notificator,
	}, 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, 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.Policy.CopiesNumbers(locationConstraint)
	if ok {
		return copiesNumbers, nil
	}

	return h.cfg.Policy.DefaultCopiesNumbers(), 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
}