package handler

import (
	"net/http"
	"net/url"
	"strconv"
	"time"

	"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"
	oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
)

// ListObjectsV1Handler handles objects listing requests for API version 1.
func (h *handler) ListObjectsV1Handler(w http.ResponseWriter, r *http.Request) {
	reqInfo := middleware.GetReqInfo(r.Context())
	params, err := parseListObjectsArgsV1(reqInfo)
	if err != nil {
		h.logAndSendError(w, "failed to parse arguments", reqInfo, err)
		return
	}

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

	list, err := h.obj.ListObjectsV1(r.Context(), params)
	if err != nil {
		h.logAndSendError(w, "something went wrong", reqInfo, err)
		return
	}

	if err = middleware.EncodeToResponse(w, h.encodeV1(params, list)); err != nil {
		h.logAndSendError(w, "something went wrong", reqInfo, err)
	}
}

func (h *handler) encodeV1(p *layer.ListObjectsParamsV1, list *layer.ListObjectsInfoV1) *ListObjectsV1Response {
	res := &ListObjectsV1Response{
		Name:         p.BktInfo.Name,
		EncodingType: p.Encode,
		Marker:       p.Marker,
		Prefix:       p.Prefix,
		MaxKeys:      p.MaxKeys,
		Delimiter:    p.Delimiter,
		IsTruncated:  list.IsTruncated,
		NextMarker:   list.NextMarker,
	}

	res.CommonPrefixes = fillPrefixes(list.Prefixes, p.Encode)

	res.Contents = fillContentsWithOwner(list.Objects, p.Encode, h.cfg.MD5Enabled())

	return res
}

// ListObjectsV2Handler handles objects listing requests for API version 2.
func (h *handler) ListObjectsV2Handler(w http.ResponseWriter, r *http.Request) {
	reqInfo := middleware.GetReqInfo(r.Context())
	params, err := parseListObjectsArgsV2(reqInfo)
	if err != nil {
		h.logAndSendError(w, "failed to parse arguments", reqInfo, err)
		return
	}

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

	list, err := h.obj.ListObjectsV2(r.Context(), params)
	if err != nil {
		h.logAndSendError(w, "something went wrong", reqInfo, err)
		return
	}

	if err = middleware.EncodeToResponse(w, h.encodeV2(params, list)); err != nil {
		h.logAndSendError(w, "something went wrong", reqInfo, err)
	}
}

func (h *handler) encodeV2(p *layer.ListObjectsParamsV2, list *layer.ListObjectsInfoV2) *ListObjectsV2Response {
	res := &ListObjectsV2Response{
		Name:                  p.BktInfo.Name,
		EncodingType:          p.Encode,
		Prefix:                s3PathEncode(p.Prefix, p.Encode),
		KeyCount:              len(list.Objects) + len(list.Prefixes),
		MaxKeys:               p.MaxKeys,
		Delimiter:             s3PathEncode(p.Delimiter, p.Encode),
		StartAfter:            s3PathEncode(p.StartAfter, p.Encode),
		IsTruncated:           list.IsTruncated,
		ContinuationToken:     p.ContinuationToken,
		NextContinuationToken: list.NextContinuationToken,
	}

	res.CommonPrefixes = fillPrefixes(list.Prefixes, p.Encode)

	res.Contents = fillContents(list.Objects, p.Encode, p.FetchOwner, h.cfg.MD5Enabled())

	return res
}

func parseListObjectsArgsV1(reqInfo *middleware.ReqInfo) (*layer.ListObjectsParamsV1, error) {
	var (
		res         layer.ListObjectsParamsV1
		queryValues = reqInfo.URL.Query()
	)

	common, err := parseListObjectArgs(reqInfo)
	if err != nil {
		return nil, err
	}
	res.ListObjectsParamsCommon = *common

	res.Marker = queryValues.Get("marker")

	return &res, nil
}

func parseListObjectsArgsV2(reqInfo *middleware.ReqInfo) (*layer.ListObjectsParamsV2, error) {
	var (
		res         layer.ListObjectsParamsV2
		queryValues = reqInfo.URL.Query()
	)

	common, err := parseListObjectArgs(reqInfo)
	if err != nil {
		return nil, err
	}
	res.ListObjectsParamsCommon = *common

	res.ContinuationToken, err = parseContinuationToken(queryValues)
	if err != nil {
		return nil, err
	}

	res.StartAfter = queryValues.Get("start-after")
	res.FetchOwner, _ = strconv.ParseBool(queryValues.Get("fetch-owner"))
	return &res, nil
}

func parseListObjectArgs(reqInfo *middleware.ReqInfo) (*layer.ListObjectsParamsCommon, error) {
	var (
		err         error
		res         layer.ListObjectsParamsCommon
		queryValues = reqInfo.URL.Query()
	)

	res.Delimiter = queryValues.Get("delimiter")
	res.Encode = queryValues.Get("encoding-type")

	if queryValues.Get("max-keys") == "" {
		res.MaxKeys = maxObjectList
	} else if res.MaxKeys, err = strconv.Atoi(queryValues.Get("max-keys")); err != nil || res.MaxKeys < 0 {
		return nil, errors.GetAPIError(errors.ErrInvalidMaxKeys)
	}

	res.Prefix = queryValues.Get("prefix")

	return &res, nil
}

func parseContinuationToken(queryValues url.Values) (string, error) {
	if val, ok := queryValues["continuation-token"]; ok {
		var objID oid.ID
		if err := objID.DecodeString(val[0]); err != nil {
			return "", errors.GetAPIError(errors.ErrIncorrectContinuationToken)
		}
		return val[0], nil
	}
	return "", nil
}

func fillPrefixes(src []string, encode string) []CommonPrefix {
	var dst []CommonPrefix
	for _, obj := range src {
		dst = append(dst, CommonPrefix{
			Prefix: s3PathEncode(obj, encode),
		})
	}
	return dst
}

func fillContentsWithOwner(src []*data.ExtendedNodeVersion, encode string, md5Enabled bool) []Object {
	return fillContents(src, encode, true, md5Enabled)
}

func fillContents(src []*data.ExtendedNodeVersion, encode string, fetchOwner, md5Enabled bool) []Object {
	var dst []Object
	for _, obj := range src {
		res := Object{
			Key:          s3PathEncode(obj.NodeVersion.FilePath, encode),
			Size:         obj.NodeVersion.Size,
			LastModified: obj.NodeVersion.Created.UTC().Format(time.RFC3339),
			ETag:         data.Quote(obj.NodeVersion.GetETag(md5Enabled)),
			StorageClass: api.DefaultStorageClass,
		}

		if fetchOwner {
			owner := obj.NodeVersion.Owner.String()
			res.Owner = &Owner{
				ID:          owner,
				DisplayName: owner,
			}
		}

		dst = append(dst, res)
	}
	return dst
}

func (h *handler) ListBucketObjectVersionsHandler(w http.ResponseWriter, r *http.Request) {
	reqInfo := middleware.GetReqInfo(r.Context())
	p, err := parseListObjectVersionsRequest(reqInfo)
	if err != nil {
		h.logAndSendError(w, "failed to parse request", reqInfo, err)
		return
	}

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

	info, err := h.obj.ListObjectVersions(r.Context(), p)
	if err != nil {
		h.logAndSendError(w, "something went wrong", reqInfo, err)
		return
	}

	response := encodeListObjectVersionsToResponse(info, p.BktInfo.Name, h.cfg.MD5Enabled())
	if err = middleware.EncodeToResponse(w, response); err != nil {
		h.logAndSendError(w, "something went wrong", reqInfo, err)
	}
}

func parseListObjectVersionsRequest(reqInfo *middleware.ReqInfo) (*layer.ListObjectVersionsParams, error) {
	var (
		err         error
		res         layer.ListObjectVersionsParams
		queryValues = reqInfo.URL.Query()
	)

	if queryValues.Get("max-keys") == "" {
		res.MaxKeys = maxObjectList
	} else if res.MaxKeys, err = strconv.Atoi(queryValues.Get("max-keys")); err != nil || res.MaxKeys <= 0 {
		return nil, errors.GetAPIError(errors.ErrInvalidMaxKeys)
	}

	res.Prefix = queryValues.Get("prefix")
	res.KeyMarker = queryValues.Get("key-marker")
	res.Delimiter = queryValues.Get("delimiter")
	res.Encode = queryValues.Get("encoding-type")
	res.VersionIDMarker = queryValues.Get("version-id-marker")

	if res.VersionIDMarker != "" && res.KeyMarker == "" {
		return nil, errors.GetAPIError(errors.VersionIDMarkerWithoutKeyMarker)
	}

	return &res, nil
}

func encodeListObjectVersionsToResponse(info *layer.ListObjectVersionsInfo, bucketName string, md5Enabled bool) *ListObjectsVersionsResponse {
	res := ListObjectsVersionsResponse{
		Name:                bucketName,
		IsTruncated:         info.IsTruncated,
		KeyMarker:           info.KeyMarker,
		NextKeyMarker:       info.NextKeyMarker,
		NextVersionIDMarker: info.NextVersionIDMarker,
		VersionIDMarker:     info.VersionIDMarker,
	}

	for _, prefix := range info.CommonPrefixes {
		res.CommonPrefixes = append(res.CommonPrefixes, CommonPrefix{Prefix: prefix})
	}

	for _, ver := range info.Version {
		res.Version = append(res.Version, ObjectVersionResponse{
			IsLatest:     ver.IsLatest,
			Key:          ver.NodeVersion.FilePath,
			LastModified: ver.NodeVersion.Created.UTC().Format(time.RFC3339),
			Owner: Owner{
				ID:          ver.NodeVersion.Owner.String(),
				DisplayName: ver.NodeVersion.Owner.String(),
			},
			Size:         ver.NodeVersion.Size,
			VersionID:    ver.Version(),
			ETag:         data.Quote(ver.NodeVersion.GetETag(md5Enabled)),
			StorageClass: api.DefaultStorageClass,
		})
	}
	// this loop is not starting till versioning is not implemented
	for _, del := range info.DeleteMarker {
		res.DeleteMarker = append(res.DeleteMarker, DeleteMarkerEntry{
			IsLatest:     del.IsLatest,
			Key:          del.NodeVersion.FilePath,
			LastModified: del.NodeVersion.Created.UTC().Format(time.RFC3339),
			Owner: Owner{
				ID:          del.NodeVersion.Owner.String(),
				DisplayName: del.NodeVersion.Owner.String(),
			},
			VersionID: del.Version(),
		})
	}

	return &res
}