package client

import (
	"context"
	"crypto/tls"
	"fmt"
	"strings"
	"time"

	"github.com/tech/sendico/pkg/merrors"
	methodsv1 "github.com/tech/sendico/pkg/proto/payments/methods/v1"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials"
	"google.golang.org/grpc/credentials/insecure"
)

// Client exposes typed helpers around the payment methods gRPC API.
type Client interface {
	CreatePaymentMethod(ctx context.Context, req *methodsv1.CreatePaymentMethodRequest) (*methodsv1.CreatePaymentMethodResponse, error)
	GetPaymentMethod(ctx context.Context, req *methodsv1.GetPaymentMethodRequest) (*methodsv1.GetPaymentMethodResponse, error)
	GetPaymentMethodPrivate(ctx context.Context, req *methodsv1.GetPaymentMethodPrivateRequest) (*methodsv1.GetPaymentMethodPrivateResponse, error)
	UpdatePaymentMethod(ctx context.Context, req *methodsv1.UpdatePaymentMethodRequest) (*methodsv1.UpdatePaymentMethodResponse, error)
	DeletePaymentMethod(ctx context.Context, req *methodsv1.DeletePaymentMethodRequest) (*methodsv1.DeletePaymentMethodResponse, error)
	SetPaymentMethodArchived(ctx context.Context, req *methodsv1.SetPaymentMethodArchivedRequest) (*methodsv1.SetPaymentMethodArchivedResponse, error)
	ListPaymentMethods(ctx context.Context, req *methodsv1.ListPaymentMethodsRequest) (*methodsv1.ListPaymentMethodsResponse, error)
	Close() error
}

type grpcPaymentMethodsClient interface {
	CreatePaymentMethod(ctx context.Context, in *methodsv1.CreatePaymentMethodRequest, opts ...grpc.CallOption) (*methodsv1.CreatePaymentMethodResponse, error)
	GetPaymentMethod(ctx context.Context, in *methodsv1.GetPaymentMethodRequest, opts ...grpc.CallOption) (*methodsv1.GetPaymentMethodResponse, error)
	GetPaymentMethodPrivate(ctx context.Context, in *methodsv1.GetPaymentMethodPrivateRequest, opts ...grpc.CallOption) (*methodsv1.GetPaymentMethodPrivateResponse, error)
	UpdatePaymentMethod(ctx context.Context, in *methodsv1.UpdatePaymentMethodRequest, opts ...grpc.CallOption) (*methodsv1.UpdatePaymentMethodResponse, error)
	DeletePaymentMethod(ctx context.Context, in *methodsv1.DeletePaymentMethodRequest, opts ...grpc.CallOption) (*methodsv1.DeletePaymentMethodResponse, error)
	SetPaymentMethodArchived(ctx context.Context, in *methodsv1.SetPaymentMethodArchivedRequest, opts ...grpc.CallOption) (*methodsv1.SetPaymentMethodArchivedResponse, error)
	ListPaymentMethods(ctx context.Context, in *methodsv1.ListPaymentMethodsRequest, opts ...grpc.CallOption) (*methodsv1.ListPaymentMethodsResponse, error)
}

type paymentMethodsClient struct {
	cfg    Config
	conn   *grpc.ClientConn
	client grpcPaymentMethodsClient
}

// New dials the payment methods endpoint and returns a ready client.
func New(ctx context.Context, cfg Config, opts ...grpc.DialOption) (Client, error) {
	cfg.setDefaults()
	if strings.TrimSpace(cfg.Address) == "" {
		return nil, merrors.InvalidArgument("payment-methods: address is required")
	}

	dialOpts := make([]grpc.DialOption, 0, len(opts)+1)
	dialOpts = append(dialOpts, opts...)
	if cfg.Insecure {
		dialOpts = append(dialOpts, grpc.WithTransportCredentials(insecure.NewCredentials()))
	} else {
		dialOpts = append(dialOpts, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{})))
	}

	conn, err := grpc.NewClient(cfg.Address, dialOpts...)
	if err != nil {
		return nil, merrors.InternalWrap(err, fmt.Sprintf("payment-methods: dial %s", cfg.Address))
	}

	return &paymentMethodsClient{
		cfg:    cfg,
		conn:   conn,
		client: methodsv1.NewPaymentMethodsServiceClient(conn),
	}, nil
}

// NewWithClient injects a pre-built payment methods client (useful for tests).
func NewWithClient(cfg Config, c grpcPaymentMethodsClient) Client {
	cfg.setDefaults()
	return &paymentMethodsClient{
		cfg:    cfg,
		client: c,
	}
}

func (c *paymentMethodsClient) Close() error {
	if c.conn != nil {
		return c.conn.Close()
	}
	return nil
}

func (c *paymentMethodsClient) CreatePaymentMethod(ctx context.Context, req *methodsv1.CreatePaymentMethodRequest) (*methodsv1.CreatePaymentMethodResponse, error) {
	timeout := c.cfg.CallTimeout
	if timeout <= 0 {
		timeout = 3 * time.Second
	}

	callCtx, cancel := context.WithTimeout(ctx, timeout)
	defer cancel()

	return c.client.CreatePaymentMethod(callCtx, req)
}

func (c *paymentMethodsClient) GetPaymentMethod(ctx context.Context, req *methodsv1.GetPaymentMethodRequest) (*methodsv1.GetPaymentMethodResponse, error) {
	timeout := c.cfg.CallTimeout
	if timeout <= 0 {
		timeout = 3 * time.Second
	}

	callCtx, cancel := context.WithTimeout(ctx, timeout)
	defer cancel()

	return c.client.GetPaymentMethod(callCtx, req)
}

func (c *paymentMethodsClient) GetPaymentMethodPrivate(ctx context.Context, req *methodsv1.GetPaymentMethodPrivateRequest) (*methodsv1.GetPaymentMethodPrivateResponse, error) {
	timeout := c.cfg.CallTimeout
	if timeout <= 0 {
		timeout = 3 * time.Second
	}

	callCtx, cancel := context.WithTimeout(ctx, timeout)
	defer cancel()

	return c.client.GetPaymentMethodPrivate(callCtx, req)
}

func (c *paymentMethodsClient) UpdatePaymentMethod(ctx context.Context, req *methodsv1.UpdatePaymentMethodRequest) (*methodsv1.UpdatePaymentMethodResponse, error) {
	timeout := c.cfg.CallTimeout
	if timeout <= 0 {
		timeout = 3 * time.Second
	}

	callCtx, cancel := context.WithTimeout(ctx, timeout)
	defer cancel()

	return c.client.UpdatePaymentMethod(callCtx, req)
}

func (c *paymentMethodsClient) DeletePaymentMethod(ctx context.Context, req *methodsv1.DeletePaymentMethodRequest) (*methodsv1.DeletePaymentMethodResponse, error) {
	timeout := c.cfg.CallTimeout
	if timeout <= 0 {
		timeout = 3 * time.Second
	}

	callCtx, cancel := context.WithTimeout(ctx, timeout)
	defer cancel()

	return c.client.DeletePaymentMethod(callCtx, req)
}

func (c *paymentMethodsClient) SetPaymentMethodArchived(ctx context.Context, req *methodsv1.SetPaymentMethodArchivedRequest) (*methodsv1.SetPaymentMethodArchivedResponse, error) {
	timeout := c.cfg.CallTimeout
	if timeout <= 0 {
		timeout = 3 * time.Second
	}

	callCtx, cancel := context.WithTimeout(ctx, timeout)
	defer cancel()

	return c.client.SetPaymentMethodArchived(callCtx, req)
}

func (c *paymentMethodsClient) ListPaymentMethods(ctx context.Context, req *methodsv1.ListPaymentMethodsRequest) (*methodsv1.ListPaymentMethodsResponse, error) {
	timeout := c.cfg.CallTimeout
	if timeout <= 0 {
		timeout = 3 * time.Second
	}

	callCtx, cancel := context.WithTimeout(ctx, timeout)
	defer cancel()

	return c.client.ListPaymentMethods(callCtx, req)
}
