package client

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

	"github.com/tech/sendico/pkg/merrors"
	fxv1 "github.com/tech/sendico/pkg/proto/common/fx/v1"
	moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
	tracev1 "github.com/tech/sendico/pkg/proto/common/trace/v1"
	oraclev1 "github.com/tech/sendico/pkg/proto/oracle/v1"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials"
	"google.golang.org/grpc/credentials/insecure"
)

// Client exposes typed helpers around the oracle gRPC API.
type Client interface {
	LatestRate(ctx context.Context, req LatestRateParams) (*RateSnapshot, error)
	GetQuote(ctx context.Context, req GetQuoteParams) (*Quote, error)
	Close() error
}

// RequestMeta carries optional multi-tenant context for oracle calls.
type RequestMeta struct {
	TenantRef       string
	OrganizationRef string
	Trace           *tracev1.TraceContext
}

type LatestRateParams struct {
	Meta     RequestMeta
	Pair     *fxv1.CurrencyPair
	Provider string
}

type RateSnapshot struct {
	Pair      *fxv1.CurrencyPair
	Mid       string
	Bid       string
	Ask       string
	SpreadBps string
	Provider  string
	RateRef   string
	AsOf      time.Time
}

type GetQuoteParams struct {
	Meta              RequestMeta
	Pair              *fxv1.CurrencyPair
	Side              fxv1.Side
	BaseAmount        *moneyv1.Money
	QuoteAmount       *moneyv1.Money
	Firm              bool
	TTL               time.Duration
	PreferredProvider string
	MaxAge            time.Duration
}

type Quote struct {
	QuoteRef    string
	Pair        *fxv1.CurrencyPair
	Side        fxv1.Side
	Price       string
	BaseAmount  *moneyv1.Money
	QuoteAmount *moneyv1.Money
	ExpiresAt   time.Time
	PricedAt    time.Time
	Provider    string
	RateRef     string
	Firm        bool
}

type grpcOracleClient interface {
	GetQuote(ctx context.Context, in *oraclev1.GetQuoteRequest, opts ...grpc.CallOption) (*oraclev1.GetQuoteResponse, error)
	LatestRate(ctx context.Context, in *oraclev1.LatestRateRequest, opts ...grpc.CallOption) (*oraclev1.LatestRateResponse, error)
}

type oracleClient struct {
	cfg    Config
	conn   *grpc.ClientConn
	client grpcOracleClient
}

// New dials the oracle 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("oracle: 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("oracle: dial %s", cfg.Address))
	}

	return &oracleClient{
		cfg:    cfg,
		conn:   conn,
		client: oraclev1.NewOracleClient(conn),
	}, nil
}

// NewWithClient injects a pre-built oracle client (useful for tests).
func NewWithClient(cfg Config, oc grpcOracleClient) Client {
	cfg.setDefaults()
	return &oracleClient{
		cfg:    cfg,
		client: oc,
	}
}

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

func (c *oracleClient) LatestRate(ctx context.Context, req LatestRateParams) (*RateSnapshot, error) {
	if req.Pair == nil {
		return nil, merrors.InvalidArgument("oracle: pair is required")
	}

	callCtx := ctx
	cancel := func() {}
	if _, hasDeadline := ctx.Deadline(); !hasDeadline {
		callCtx, cancel = context.WithTimeout(ctx, c.cfg.CallTimeout)
	}
	defer cancel()

	resp, err := c.client.LatestRate(callCtx, &oraclev1.LatestRateRequest{
		Meta:     toProtoMeta(req.Meta),
		Pair:     req.Pair,
		Provider: req.Provider,
	})
	if err != nil {
		return nil, merrors.InternalWrap(err, "oracle: latest rate")
	}
	if resp.GetRate() == nil {
		return nil, merrors.Internal("oracle: latest rate: empty payload")
	}
	return fromProtoRate(resp.GetRate()), nil
}

func (c *oracleClient) GetQuote(ctx context.Context, req GetQuoteParams) (*Quote, error) {
	if req.Pair == nil {
		return nil, merrors.InvalidArgument("oracle: pair is required")
	}
	if req.Side == fxv1.Side_SIDE_UNSPECIFIED {
		return nil, merrors.InvalidArgument("oracle: side is required")
	}

	baseSupplied := req.BaseAmount != nil
	quoteSupplied := req.QuoteAmount != nil
	if baseSupplied == quoteSupplied {
		return nil, merrors.InvalidArgument("oracle: exactly one of base_amount or quote_amount must be set")
	}

	callCtx := ctx
	cancel := func() {}
	if _, hasDeadline := ctx.Deadline(); !hasDeadline {
		callCtx, cancel = context.WithTimeout(ctx, c.cfg.CallTimeout)
	}
	defer cancel()

	protoReq := &oraclev1.GetQuoteRequest{
		Meta:              toProtoMeta(req.Meta),
		Pair:              req.Pair,
		Side:              req.Side,
		Firm:              req.Firm,
		PreferredProvider: req.PreferredProvider,
	}
	if req.TTL > 0 {
		protoReq.TtlMs = req.TTL.Milliseconds()
	}
	if req.MaxAge > 0 {
		maxAgeMs := req.MaxAge.Milliseconds()
		if maxAgeMs > math.MaxInt32 {
			maxAgeMs = math.MaxInt32
		}
		protoReq.MaxAgeMs = int32(maxAgeMs)
	}
	if baseSupplied {
		protoReq.AmountInput = &oraclev1.GetQuoteRequest_BaseAmount{BaseAmount: req.BaseAmount}
	} else {
		protoReq.AmountInput = &oraclev1.GetQuoteRequest_QuoteAmount{QuoteAmount: req.QuoteAmount}
	}

	resp, err := c.client.GetQuote(callCtx, protoReq)
	if err != nil {
		return nil, merrors.InternalWrap(err, "oracle: get quote")
	}
	if resp.GetQuote() == nil {
		return nil, merrors.Internal("oracle: get quote: empty payload")
	}
	return fromProtoQuote(resp.GetQuote()), nil
}

func toProtoMeta(meta RequestMeta) *oraclev1.RequestMeta {
	if meta.TenantRef == "" && meta.OrganizationRef == "" && meta.Trace == nil {
		return nil
	}
	return &oraclev1.RequestMeta{
		TenantRef:       meta.TenantRef,
		OrganizationRef: meta.OrganizationRef,
		Trace:           meta.Trace,
	}
}

func fromProtoRate(rate *oraclev1.RateSnapshot) *RateSnapshot {
	if rate == nil {
		return nil
	}
	return &RateSnapshot{
		Pair:      rate.Pair,
		Mid:       rate.GetMid().GetValue(),
		Bid:       rate.GetBid().GetValue(),
		Ask:       rate.GetAsk().GetValue(),
		SpreadBps: rate.GetSpreadBps().GetValue(),
		Provider:  rate.GetProvider(),
		RateRef:   rate.GetRateRef(),
		AsOf:      time.UnixMilli(rate.GetAsofUnixMs()),
	}
}

func fromProtoQuote(quote *oraclev1.Quote) *Quote {
	if quote == nil {
		return nil
	}
	pricedAt := time.Time{}
	if ts := quote.GetPricedAt(); ts != nil {
		pricedAt = ts.AsTime()
	}
	return &Quote{
		QuoteRef:    quote.GetQuoteRef(),
		Pair:        quote.Pair,
		Side:        quote.GetSide(),
		Price:       quote.GetPrice().GetValue(),
		BaseAmount:  quote.BaseAmount,
		QuoteAmount: quote.QuoteAmount,
		ExpiresAt:   time.UnixMilli(quote.GetExpiresAtUnixMs()),
		PricedAt:    pricedAt,
		Provider:    quote.GetProvider(),
		RateRef:     quote.GetRateRef(),
		Firm:        quote.GetFirm(),
	}
}
