package gsresponse

import (
	"context"
	"errors"
	"fmt"

	"github.com/tech/sendico/pkg/merrors"
	"github.com/tech/sendico/pkg/mlogger"
	"github.com/tech/sendico/pkg/mservice"
	"go.uber.org/zap"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
)

// Responder produces a response or a gRPC status error when executed.
type Responder[T any] func(ctx context.Context) (*T, error)

func message(err error) string {
	if err == nil {
		return ""
	}
	return err.Error()
}

func Success[T any](resp *T) Responder[T] {
	return func(context.Context) (*T, error) {
		return resp, nil
	}
}

func Empty[T any]() Responder[T] {
	return func(context.Context) (*T, error) {
		return nil, nil
	}
}

func Error[T any](logger mlogger.Logger, service mservice.Type, code codes.Code, hint string, err error) Responder[T] {
	return func(ctx context.Context) (*T, error) {
		fields := []zap.Field{
			zap.String("service", service),
			zap.String("status_code", code.String()),
		}
		if hint != "" {
			fields = append(fields, zap.String("error_hint", hint))
		}
		if err != nil {
			fields = append(fields, zap.Error(err))
		}
		logger.Warn("GRPC request failed", fields...)

		msg := message(err)
		switch {
		case hint == "" && msg == "":
			return nil, status.Error(code, code.String())
		case hint == "":
			return nil, status.Error(code, msg)
		case msg == "":
			return nil, status.Error(code, hint)
		default:
			return nil, status.Error(code, fmt.Sprintf("%s: %s", hint, msg))
		}
	}
}

func Internal[T any](logger mlogger.Logger, service mservice.Type, err error) Responder[T] {
	return Error[T](logger, service, codes.Internal, "internal_error", err)
}

func InvalidArgument[T any](logger mlogger.Logger, service mservice.Type, err error) Responder[T] {
	return Error[T](logger, service, codes.InvalidArgument, "invalid_argument", err)
}

func NotFound[T any](logger mlogger.Logger, service mservice.Type, err error) Responder[T] {
	return Error[T](logger, service, codes.NotFound, "not_found", err)
}

func Unauthorized[T any](logger mlogger.Logger, service mservice.Type, err error) Responder[T] {
	return Error[T](logger, service, codes.Unauthenticated, "unauthorized", err)
}

func PermissionDenied[T any](logger mlogger.Logger, service mservice.Type, err error) Responder[T] {
	return Error[T](logger, service, codes.PermissionDenied, "access_denied", err)
}

func FailedPrecondition[T any](logger mlogger.Logger, service mservice.Type, hint string, err error) Responder[T] {
	return Error[T](logger, service, codes.FailedPrecondition, hint, err)
}

func Conflict[T any](logger mlogger.Logger, service mservice.Type, err error) Responder[T] {
	return Error[T](logger, service, codes.Aborted, "conflict", err)
}

func DeadlineExceeded[T any](logger mlogger.Logger, service mservice.Type, err error) Responder[T] {
	return Error[T](logger, service, codes.DeadlineExceeded, "deadline_exceeded", err)
}

func Unavailable[T any](logger mlogger.Logger, service mservice.Type, err error) Responder[T] {
	return Error[T](logger, service, codes.Unavailable, "service_unavailable", err)
}

func Unimplemented[T any](logger mlogger.Logger, service mservice.Type, err error) Responder[T] {
	return Error[T](logger, service, codes.Unimplemented, "not_implemented", err)
}

func AlreadyExists[T any](logger mlogger.Logger, service mservice.Type, err error) Responder[T] {
	return Error[T](logger, service, codes.AlreadyExists, "already_exists", err)
}

func Auto[T any](logger mlogger.Logger, service mservice.Type, err error) Responder[T] {
	switch {
	case err == nil:
		return Empty[T]()
	case errors.Is(err, merrors.ErrInvalidArg):
		return InvalidArgument[T](logger, service, err)
	case errors.Is(err, merrors.ErrAccessDenied):
		return PermissionDenied[T](logger, service, err)
	case errors.Is(err, merrors.ErrNoData):
		return NotFound[T](logger, service, err)
	case errors.Is(err, merrors.ErrUnauthorized):
		return Unauthorized[T](logger, service, err)
	case errors.Is(err, merrors.ErrDataConflict):
		return Conflict[T](logger, service, err)
	default:
		return Internal[T](logger, service, err)
	}
}

func Execute[T any](ctx context.Context, responder Responder[T]) (*T, error) {
	if responder == nil {
		return nil, status.Error(codes.Internal, "missing responder")
	}
	return responder(ctx)
}

func Unary[TReq any, TResp any](logger mlogger.Logger, service mservice.Type, handler func(context.Context, *TReq) Responder[TResp]) func(context.Context, *TReq) (*TResp, error) {
	return func(ctx context.Context, req *TReq) (*TResp, error) {
		if handler == nil {
			return nil, status.Error(codes.Internal, "missing handler")
		}
		responder := handler(ctx, req)
		return Execute(ctx, responder)
	}
}
