package handler

import (
	"encoding/base64"
	"encoding/hex"
	"fmt"
	"net/http"
	"strconv"
	"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"
	"go.uber.org/zap"
)

type (
	GetObjectAttributesResponse struct {
		ETag         string       `xml:"ETag,omitempty"`
		Checksum     *Checksum    `xml:"Checksum,omitempty"`
		ObjectSize   uint64       `xml:"ObjectSize,omitempty"`
		StorageClass string       `xml:"StorageClass,omitempty"`
		ObjectParts  *ObjectParts `xml:"ObjectParts,omitempty"`
	}

	Checksum struct {
		ChecksumSHA256 string `xml:"ChecksumSHA256,omitempty"`
	}

	ObjectParts struct {
		IsTruncated          bool   `xml:"IsTruncated,omitempty"`
		MaxParts             int    `xml:"MaxParts,omitempty"`
		NextPartNumberMarker int    `xml:"NextPartNumberMarker,omitempty"`
		PartNumberMarker     int    `xml:"PartNumberMarker,omitempty"`
		Parts                []Part `xml:"Part,omitempty"`
		PartsCount           int    `xml:"PartsCount,omitempty"`
	}

	Part struct {
		ChecksumSHA256 string `xml:"ChecksumSHA256,omitempty"`
		PartNumber     int    `xml:"PartNumber,omitempty"`
		Size           int    `xml:"Size,omitempty"`
	}

	GetObjectAttributesArgs struct {
		MaxParts         int
		PartNumberMarker int
		Attributes       []string
		VersionID        string
		Conditional      *conditionalArgs
	}
)

const (
	eTag         = "ETag"
	checksum     = "Checksum"
	objectParts  = "ObjectParts"
	storageClass = "StorageClass"
	objectSize   = "ObjectSize"
)

var validAttributes = map[string]struct{}{
	eTag:         {},
	checksum:     {},
	objectParts:  {},
	storageClass: {},
	objectSize:   {},
}

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

	params, err := parseGetObjectAttributeArgs(r)
	if err != nil {
		h.logAndSendError(w, "invalid request", reqInfo, err)
		return
	}

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

	p := &layer.HeadObjectParams{
		BktInfo:   bktInfo,
		Object:    reqInfo.ObjectName,
		VersionID: params.VersionID,
	}

	extendedInfo, err := h.obj.GetExtendedObjectInfo(r.Context(), p)
	if err != nil {
		h.logAndSendError(w, "could not fetch object info", reqInfo, err)
		return
	}
	info := extendedInfo.ObjectInfo

	encryptionParams, err := formEncryptionParams(r)
	if err != nil {
		h.logAndSendError(w, "invalid sse headers", reqInfo, err)
		return
	}

	if err = encryptionParams.MatchObjectEncryption(layer.FormEncryptionInfo(info.Headers)); err != nil {
		h.logAndSendError(w, "encryption doesn't match object", reqInfo, errors.GetAPIError(errors.ErrBadRequest), zap.Error(err))
		return
	}

	if err = checkPreconditions(info, params.Conditional, h.cfg.MD5Enabled()); err != nil {
		h.logAndSendError(w, "precondition failed", reqInfo, err)
		return
	}

	bktSettings, err := h.obj.GetBucketSettings(r.Context(), bktInfo)
	if err != nil {
		h.logAndSendError(w, "could not get bucket settings", reqInfo, err)
		return
	}

	response, err := encodeToObjectAttributesResponse(info, params, h.cfg.MD5Enabled())
	if err != nil {
		h.logAndSendError(w, "couldn't encode object info to response", reqInfo, err)
		return
	}

	writeAttributesHeaders(w.Header(), extendedInfo, bktSettings.Unversioned())
	if err = middleware.EncodeToResponse(w, response); err != nil {
		h.logAndSendError(w, "something went wrong", reqInfo, err)
	}
}

func writeAttributesHeaders(h http.Header, info *data.ExtendedObjectInfo, isBucketUnversioned bool) {
	h.Set(api.LastModified, info.ObjectInfo.Created.UTC().Format(http.TimeFormat))
	if !isBucketUnversioned {
		h.Set(api.AmzVersionID, info.Version())
	}

	if info.NodeVersion.IsDeleteMarker {
		h.Set(api.AmzDeleteMarker, strconv.FormatBool(true))
	}

	// x-amz-request-charged
}

func parseGetObjectAttributeArgs(r *http.Request) (*GetObjectAttributesArgs, error) {
	res := &GetObjectAttributesArgs{
		VersionID: r.URL.Query().Get(api.QueryVersionID),
	}

	attributesVal := r.Header.Get(api.AmzObjectAttributes)
	if attributesVal == "" {
		return nil, errors.GetAPIError(errors.ErrInvalidAttributeName)
	}

	attributes := strings.Split(attributesVal, ",")
	for _, a := range attributes {
		if _, ok := validAttributes[a]; !ok {
			return nil, errors.GetAPIError(errors.ErrInvalidAttributeName)
		}
		res.Attributes = append(res.Attributes, a)
	}

	var err error
	maxPartsVal := r.Header.Get(api.AmzMaxParts)
	if maxPartsVal == "" {
		res.MaxParts = layer.MaxSizePartsList
	} else if res.MaxParts, err = strconv.Atoi(maxPartsVal); err != nil || res.MaxParts < 0 {
		return nil, errors.GetAPIError(errors.ErrInvalidMaxKeys)
	}

	markerVal := r.Header.Get(api.AmzPartNumberMarker)
	if markerVal != "" {
		if res.PartNumberMarker, err = strconv.Atoi(markerVal); err != nil || res.PartNumberMarker < 0 {
			return nil, errors.GetAPIError(errors.ErrInvalidPartNumberMarker)
		}
	}

	res.Conditional, err = parseConditionalHeaders(r.Header)
	return res, err
}

func encodeToObjectAttributesResponse(info *data.ObjectInfo, p *GetObjectAttributesArgs, md5Enabled bool) (*GetObjectAttributesResponse, error) {
	resp := &GetObjectAttributesResponse{}

	for _, attr := range p.Attributes {
		switch attr {
		case eTag:
			resp.ETag = data.Quote(info.ETag(md5Enabled))
		case storageClass:
			resp.StorageClass = api.DefaultStorageClass
		case objectSize:
			resp.ObjectSize = info.Size
		case checksum:
			checksumBytes, err := hex.DecodeString(info.HashSum)
			if err != nil {
				return nil, fmt.Errorf("form upload attributes: %w", err)
			}
			resp.Checksum = &Checksum{ChecksumSHA256: base64.StdEncoding.EncodeToString(checksumBytes)}
		case objectParts:
			parts, err := formUploadAttributes(info, p.MaxParts, p.PartNumberMarker)
			if err != nil {
				return nil, fmt.Errorf("form upload attributes: %w", err)
			}
			if parts != nil {
				resp.ObjectParts = parts
			}
		}
	}

	return resp, nil
}

func formUploadAttributes(info *data.ObjectInfo, maxParts, marker int) (*ObjectParts, error) {
	completedParts, ok := info.Headers[layer.UploadCompletedParts]
	if !ok {
		return nil, nil
	}

	partInfos := strings.Split(completedParts, ",")
	parts := make([]Part, len(partInfos))
	for i, p := range partInfos {
		part, err := layer.ParseCompletedPartHeader(p)
		if err != nil {
			return nil, fmt.Errorf("invalid completed part: %w", err)
		}
		// ETag value contains SHA256 checksum.
		checksumBytes, err := hex.DecodeString(part.ETag)
		if err != nil {
			return nil, fmt.Errorf("invalid sha256 checksum in completed part: %w", err)
		}
		parts[i] = Part{
			PartNumber:     part.PartNumber,
			Size:           int(part.Size),
			ChecksumSHA256: base64.StdEncoding.EncodeToString(checksumBytes),
		}
	}

	res := &ObjectParts{
		PartsCount: len(parts),
	}

	if marker != 0 {
		res.PartNumberMarker = marker
		var found bool
		for i, n := range parts {
			if n.PartNumber == marker {
				parts = parts[i:]
				found = true
				break
			}
		}
		if !found {
			return nil, errors.GetAPIError(errors.ErrInvalidPartNumberMarker)
		}
	}

	res.MaxParts = maxParts
	if len(parts) > maxParts {
		res.IsTruncated = true
		res.NextPartNumberMarker = parts[maxParts].PartNumber
		parts = parts[:maxParts]
	}

	res.Parts = parts

	return res, nil
}