package handler

import (
	"context"
	"errors"
	"fmt"
	"net/http"
	"strconv"
	"strings"

	"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api"
	"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data"
	s3errors "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"
	frosterrors "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/frostfs/errors"
	"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/session"
	"go.opentelemetry.io/otel/trace"
	"go.uber.org/zap"
)

func (h *handler) reqLogger(ctx context.Context) *zap.Logger {
	reqLogger := middleware.GetReqLog(ctx)
	if reqLogger != nil {
		return reqLogger
	}
	return h.log
}

func (h *handler) logAndSendError(w http.ResponseWriter, logText string, reqInfo *middleware.ReqInfo, err error, additional ...zap.Field) {
	err = handleDeleteMarker(w, err)
	if code, wrErr := middleware.WriteErrorResponse(w, reqInfo, transformToS3Error(err)); wrErr != nil {
		additional = append(additional, zap.NamedError("write_response_error", wrErr))
	} else {
		additional = append(additional, zap.Int("status", code))
	}
	fields := []zap.Field{
		zap.String("request_id", reqInfo.RequestID),
		zap.String("method", reqInfo.API),
		zap.String("bucket", reqInfo.BucketName),
		zap.String("object", reqInfo.ObjectName),
		zap.String("description", logText),
		zap.Error(err)}
	fields = append(fields, additional...)
	if traceID, err := trace.TraceIDFromHex(reqInfo.TraceID); err == nil && traceID.IsValid() {
		fields = append(fields, zap.String("trace_id", reqInfo.TraceID))
	}
	h.log.Error(logs.RequestFailed, fields...) // consider using h.reqLogger (it requires accept context.Context or http.Request)
}

func handleDeleteMarker(w http.ResponseWriter, err error) error {
	var target layer.DeleteMarkerError
	if !errors.As(err, &target) {
		return err
	}

	w.Header().Set(api.AmzDeleteMarker, "true")
	return fmt.Errorf("%w: %s", s3errors.GetAPIError(target.ErrorCode), err)
}

func transformToS3Error(err error) error {
	err = frosterrors.UnwrapErr(err) // this wouldn't work with errors.Join
	if _, ok := err.(s3errors.Error); ok {
		return err
	}

	if errors.Is(err, layer.ErrAccessDenied) ||
		errors.Is(err, layer.ErrNodeAccessDenied) {
		return s3errors.GetAPIError(s3errors.ErrAccessDenied)
	}

	if errors.Is(err, layer.ErrGatewayTimeout) {
		return s3errors.GetAPIError(s3errors.ErrGatewayTimeout)
	}

	return s3errors.GetAPIError(s3errors.ErrInternalError)
}

func (h *handler) ResolveBucket(ctx context.Context, bucket string) (*data.BucketInfo, error) {
	return h.obj.GetBucketInfo(ctx, bucket)
}

func (h *handler) ResolveCID(ctx context.Context, bucket string) (cid.ID, error) {
	return h.obj.ResolveCID(ctx, bucket)
}

func (h *handler) getBucketAndCheckOwner(r *http.Request, bucket string, header ...string) (*data.BucketInfo, error) {
	bktInfo, err := h.obj.GetBucketInfo(r.Context(), bucket)
	if err != nil {
		return nil, err
	}

	var expected string
	if len(header) == 0 {
		expected = r.Header.Get(api.AmzExpectedBucketOwner)
	} else {
		expected = r.Header.Get(header[0])
	}

	if len(expected) == 0 {
		return bktInfo, nil
	}

	return bktInfo, checkOwner(bktInfo, expected)
}

func parseRange(s string) (*layer.RangeParams, error) {
	if s == "" {
		return nil, nil
	}

	prefix := "bytes="

	if !strings.HasPrefix(s, prefix) {
		return nil, s3errors.GetAPIError(s3errors.ErrInvalidRange)
	}

	s = strings.TrimPrefix(s, prefix)

	valuesStr := strings.Split(s, "-")
	if len(valuesStr) != 2 {
		return nil, s3errors.GetAPIError(s3errors.ErrInvalidRange)
	}

	values := make([]uint64, 0, len(valuesStr))
	for _, v := range valuesStr {
		num, err := strconv.ParseUint(v, 10, 64)
		if err != nil {
			return nil, s3errors.GetAPIError(s3errors.ErrInvalidRange)
		}
		values = append(values, num)
	}
	if values[0] > values[1] {
		return nil, s3errors.GetAPIError(s3errors.ErrInvalidRange)
	}

	return &layer.RangeParams{
		Start: values[0],
		End:   values[1],
	}, nil
}

func getSessionTokenSetEACL(ctx context.Context) (*session.Container, error) {
	boxData, err := middleware.GetBoxData(ctx)
	if err != nil {
		return nil, err
	}
	sessionToken := boxData.Gate.SessionTokenForSetEACL()
	if sessionToken == nil {
		return nil, s3errors.GetAPIError(s3errors.ErrAccessDenied)
	}

	return sessionToken, nil
}