package client

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

	"github.com/tech/sendico/pkg/merrors"
	ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials"
	"google.golang.org/grpc/credentials/insecure"
)

// Client exposes typed helpers around the ledger gRPC API.
type Client interface {
	PostCreditWithCharges(ctx context.Context, req *ledgerv1.PostCreditRequest) (*ledgerv1.PostResponse, error)
	PostDebitWithCharges(ctx context.Context, req *ledgerv1.PostDebitRequest) (*ledgerv1.PostResponse, error)
	TransferInternal(ctx context.Context, req *ledgerv1.TransferRequest) (*ledgerv1.PostResponse, error)
	ApplyFXWithCharges(ctx context.Context, req *ledgerv1.FXRequest) (*ledgerv1.PostResponse, error)

	GetBalance(ctx context.Context, req *ledgerv1.GetBalanceRequest) (*ledgerv1.BalanceResponse, error)
	GetJournalEntry(ctx context.Context, req *ledgerv1.GetEntryRequest) (*ledgerv1.JournalEntryResponse, error)
	GetStatement(ctx context.Context, req *ledgerv1.GetStatementRequest) (*ledgerv1.StatementResponse, error)

	Close() error
}

type grpcLedgerClient interface {
	PostCreditWithCharges(ctx context.Context, in *ledgerv1.PostCreditRequest, opts ...grpc.CallOption) (*ledgerv1.PostResponse, error)
	PostDebitWithCharges(ctx context.Context, in *ledgerv1.PostDebitRequest, opts ...grpc.CallOption) (*ledgerv1.PostResponse, error)
	TransferInternal(ctx context.Context, in *ledgerv1.TransferRequest, opts ...grpc.CallOption) (*ledgerv1.PostResponse, error)
	ApplyFXWithCharges(ctx context.Context, in *ledgerv1.FXRequest, opts ...grpc.CallOption) (*ledgerv1.PostResponse, error)
	GetBalance(ctx context.Context, in *ledgerv1.GetBalanceRequest, opts ...grpc.CallOption) (*ledgerv1.BalanceResponse, error)
	GetJournalEntry(ctx context.Context, in *ledgerv1.GetEntryRequest, opts ...grpc.CallOption) (*ledgerv1.JournalEntryResponse, error)
	GetStatement(ctx context.Context, in *ledgerv1.GetStatementRequest, opts ...grpc.CallOption) (*ledgerv1.StatementResponse, error)
}

type ledgerClient struct {
	cfg    Config
	conn   *grpc.ClientConn
	client grpcLedgerClient
}

// New dials the ledger 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("ledger: address is required")
	}

	dialCtx, cancel := context.WithTimeout(ctx, cfg.DialTimeout)
	defer cancel()

	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.DialContext(dialCtx, cfg.Address, dialOpts...)
	if err != nil {
		return nil, merrors.InternalWrap(err, fmt.Sprintf("ledger: dial %s", cfg.Address))
	}

	return &ledgerClient{
		cfg:    cfg,
		conn:   conn,
		client: ledgerv1.NewLedgerServiceClient(conn),
	}, nil
}

// NewWithClient injects a pre-built ledger client (useful for tests).
func NewWithClient(cfg Config, lc grpcLedgerClient) Client {
	cfg.setDefaults()
	return &ledgerClient{
		cfg:    cfg,
		client: lc,
	}
}

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

func (c *ledgerClient) PostCreditWithCharges(ctx context.Context, req *ledgerv1.PostCreditRequest) (*ledgerv1.PostResponse, error) {
	ctx, cancel := c.callContext(ctx)
	defer cancel()
	return c.client.PostCreditWithCharges(ctx, req)
}

func (c *ledgerClient) PostDebitWithCharges(ctx context.Context, req *ledgerv1.PostDebitRequest) (*ledgerv1.PostResponse, error) {
	ctx, cancel := c.callContext(ctx)
	defer cancel()
	return c.client.PostDebitWithCharges(ctx, req)
}

func (c *ledgerClient) TransferInternal(ctx context.Context, req *ledgerv1.TransferRequest) (*ledgerv1.PostResponse, error) {
	ctx, cancel := c.callContext(ctx)
	defer cancel()
	return c.client.TransferInternal(ctx, req)
}

func (c *ledgerClient) ApplyFXWithCharges(ctx context.Context, req *ledgerv1.FXRequest) (*ledgerv1.PostResponse, error) {
	ctx, cancel := c.callContext(ctx)
	defer cancel()
	return c.client.ApplyFXWithCharges(ctx, req)
}

func (c *ledgerClient) GetBalance(ctx context.Context, req *ledgerv1.GetBalanceRequest) (*ledgerv1.BalanceResponse, error) {
	ctx, cancel := c.callContext(ctx)
	defer cancel()
	return c.client.GetBalance(ctx, req)
}

func (c *ledgerClient) GetJournalEntry(ctx context.Context, req *ledgerv1.GetEntryRequest) (*ledgerv1.JournalEntryResponse, error) {
	ctx, cancel := c.callContext(ctx)
	defer cancel()
	return c.client.GetJournalEntry(ctx, req)
}

func (c *ledgerClient) GetStatement(ctx context.Context, req *ledgerv1.GetStatementRequest) (*ledgerv1.StatementResponse, error) {
	ctx, cancel := c.callContext(ctx)
	defer cancel()
	return c.client.GetStatement(ctx, req)
}

func (c *ledgerClient) callContext(ctx context.Context) (context.Context, context.CancelFunc) {
	timeout := c.cfg.CallTimeout
	if timeout <= 0 {
		timeout = 3 * time.Second
	}
	return context.WithTimeout(ctx, timeout)
}
