package handler

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

const maxPatchSize = 5 * 1024 * 1024 * 1024 // 5GB

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

	if _, ok := r.Header[api.ContentRange]; !ok {
		h.logAndSendError(w, "missing Content-Range", reqInfo, errors.GetAPIError(errors.ErrMissingContentRange))
		return
	}

	if _, ok := r.Header[api.ContentLength]; !ok {
		h.logAndSendError(w, "missing Content-Length", reqInfo, errors.GetAPIError(errors.ErrMissingContentLength))
		return
	}

	conditional, err := parsePatchConditionalHeaders(r.Header)
	if err != nil {
		h.logAndSendError(w, "could not parse conditional headers", reqInfo, err)
		return
	}

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

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

	srcObjPrm := &layer.HeadObjectParams{
		Object:    reqInfo.ObjectName,
		BktInfo:   bktInfo,
		VersionID: reqInfo.URL.Query().Get(api.QueryVersionID),
	}

	extendedSrcObjInfo, err := h.obj.GetExtendedObjectInfo(ctx, srcObjPrm)
	if err != nil {
		h.logAndSendError(w, "could not find object", reqInfo, err)
		return
	}
	srcObjInfo := extendedSrcObjInfo.ObjectInfo

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

	srcSize, err := layer.GetObjectSize(srcObjInfo)
	if err != nil {
		h.logAndSendError(w, "failed to get source object size", reqInfo, err)
		return
	}

	byteRange, err := parsePatchByteRange(r.Header.Get(api.ContentRange), srcSize)
	if err != nil {
		h.logAndSendError(w, "could not parse byte range", reqInfo, errors.GetAPIError(errors.ErrInvalidRange), zap.Error(err))
		return
	}

	if maxPatchSize < byteRange.End-byteRange.Start+1 {
		h.logAndSendError(w, "byte range length is longer than allowed", reqInfo, errors.GetAPIError(errors.ErrInvalidRange), zap.Error(err))
		return
	}

	if uint64(r.ContentLength) != (byteRange.End - byteRange.Start + 1) {
		h.logAndSendError(w, "content-length must be equal to byte range length", reqInfo, errors.GetAPIError(errors.ErrInvalidRangeLength))
		return
	}

	if byteRange.Start > srcSize {
		h.logAndSendError(w, "start byte is greater than object size", reqInfo, errors.GetAPIError(errors.ErrRangeOutOfBounds))
		return
	}

	params := &layer.PatchObjectParams{
		Object:            extendedSrcObjInfo,
		BktInfo:           bktInfo,
		NewBytes:          r.Body,
		Range:             byteRange,
		VersioningEnabled: settings.VersioningEnabled(),
	}

	params.CopiesNumbers, err = h.pickCopiesNumbers(nil, reqInfo.Namespace, bktInfo.LocationConstraint)
	if err != nil {
		h.logAndSendError(w, "invalid copies number", reqInfo, err)
		return
	}

	extendedObjInfo, err := h.obj.PatchObject(ctx, params)
	if err != nil {
		if isErrObjectLocked(err) {
			h.logAndSendError(w, "object is locked", reqInfo, errors.GetAPIError(errors.ErrAccessDenied))
		} else {
			h.logAndSendError(w, "could not patch object", reqInfo, err)
		}
		return
	}

	if settings.VersioningEnabled() {
		w.Header().Set(api.AmzVersionID, extendedObjInfo.ObjectInfo.VersionID())
	}

	w.Header().Set(api.ETag, data.Quote(extendedObjInfo.ObjectInfo.ETag(h.cfg.MD5Enabled())))

	resp := PatchObjectResult{
		Object: PatchObject{
			LastModified: extendedObjInfo.ObjectInfo.Created.UTC().Format(time.RFC3339),
			ETag:         data.Quote(extendedObjInfo.ObjectInfo.ETag(h.cfg.MD5Enabled())),
		},
	}

	if err = middleware.EncodeToResponse(w, resp); err != nil {
		h.logAndSendError(w, "could not encode PatchObjectResult to response", reqInfo, err)
		return
	}
}

func parsePatchConditionalHeaders(headers http.Header) (*conditionalArgs, error) {
	var err error
	args := &conditionalArgs{
		IfMatch: data.UnQuote(headers.Get(api.IfMatch)),
	}

	if args.IfUnmodifiedSince, err = parseHTTPTime(headers.Get(api.IfUnmodifiedSince)); err != nil {
		return nil, err
	}

	return args, nil
}

func parsePatchByteRange(rangeStr string, objSize uint64) (*layer.RangeParams, error) {
	const prefix = "bytes "

	if rangeStr == "" {
		return nil, fmt.Errorf("empty range")
	}

	if !strings.HasPrefix(rangeStr, prefix) {
		return nil, fmt.Errorf("unknown unit in range header")
	}

	rangeStr, _, found := strings.Cut(strings.TrimPrefix(rangeStr, prefix), "/") // value after / is ignored
	if !found {
		return nil, fmt.Errorf("invalid range: %s", rangeStr)
	}

	startStr, endStr, found := strings.Cut(rangeStr, "-")
	if !found {
		return nil, fmt.Errorf("invalid range: %s", rangeStr)
	}

	start, err := strconv.ParseUint(startStr, 10, 64)
	if err != nil {
		return nil, fmt.Errorf("invalid start byte: %s", startStr)
	}

	end := objSize - 1
	if len(endStr) > 0 {
		end, err = strconv.ParseUint(endStr, 10, 64)
		if err != nil {
			return nil, fmt.Errorf("invalid end byte: %s", endStr)
		}
	}

	if start > end {
		return nil, fmt.Errorf("start byte is greater than end byte")
	}

	return &layer.RangeParams{
		Start: start,
		End:   end,
	}, nil
}