package client

import (
	"context"
	"strings"
	"time"

	"github.com/shopspring/decimal"
	"github.com/tech/sendico/pkg/merrors"
	"github.com/tech/sendico/pkg/mlogger"
	"github.com/tech/sendico/pkg/model/account_role"
	moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
	connectorv1 "github.com/tech/sendico/pkg/proto/connector/v1"
	mntxv1 "github.com/tech/sendico/pkg/proto/gateway/mntx/v1"
	"go.uber.org/zap"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
	"google.golang.org/protobuf/types/known/structpb"
)

// Client wraps the Monetix gateway gRPC API.
type Client interface {
	CreateCardPayout(ctx context.Context, req *mntxv1.CardPayoutRequest) (*mntxv1.CardPayoutResponse, error)
	CreateCardTokenPayout(ctx context.Context, req *mntxv1.CardTokenPayoutRequest) (*mntxv1.CardTokenPayoutResponse, error)
	GetCardPayoutStatus(ctx context.Context, req *mntxv1.GetCardPayoutStatusRequest) (*mntxv1.GetCardPayoutStatusResponse, error)
	ListGatewayInstances(ctx context.Context, req *mntxv1.ListGatewayInstancesRequest) (*mntxv1.ListGatewayInstancesResponse, error)
	Close() error
}

type grpcConnectorClient interface {
	SubmitOperation(ctx context.Context, in *connectorv1.SubmitOperationRequest, opts ...grpc.CallOption) (*connectorv1.SubmitOperationResponse, error)
	GetOperation(ctx context.Context, in *connectorv1.GetOperationRequest, opts ...grpc.CallOption) (*connectorv1.GetOperationResponse, error)
}

type gatewayClient struct {
	conn   *grpc.ClientConn
	client grpcConnectorClient
	cfg    Config
	logger mlogger.Logger
}

// New dials the Monetix gateway.
func New(ctx context.Context, cfg Config, opts ...grpc.DialOption) (Client, error) {
	cfg.setDefaults()
	if strings.TrimSpace(cfg.Address) == "" {
		return nil, merrors.InvalidArgument("mcards: address is required")
	}
	dialOpts := make([]grpc.DialOption, 0, len(opts)+1)
	dialOpts = append(dialOpts, grpc.WithTransportCredentials(insecure.NewCredentials()))
	dialOpts = append(dialOpts, opts...)

	conn, err := grpc.NewClient(cfg.Address, dialOpts...)
	if err != nil {
		return nil, merrors.Internal("mcards: dial failed: " + err.Error())
	}

	return &gatewayClient{
		conn:   conn,
		client: connectorv1.NewConnectorServiceClient(conn),
		cfg:    cfg,
		logger: cfg.Logger,
	}, nil
}

func (g *gatewayClient) Close() error {
	if g.conn != nil {
		return g.conn.Close()
	}
	return nil
}

func (g *gatewayClient) callContext(ctx context.Context, method string) (context.Context, context.CancelFunc) {
	if ctx == nil {
		ctx = context.Background()
	}
	timeout := g.cfg.CallTimeout
	if timeout <= 0 {
		timeout = 5 * time.Second
	}
	if g.logger != nil {
		fields := []zap.Field{
			zap.String("method", method),
			zap.Duration("timeout", timeout),
		}
		if deadline, ok := ctx.Deadline(); ok {
			fields = append(fields, zap.Time("parent_deadline", deadline), zap.Duration("parent_deadline_in", time.Until(deadline)))
		}
		g.logger.Info("Mntx gateway client call timeout applied", fields...)
	}
	return context.WithTimeout(ctx, timeout) //nolint:gosec // cancel func is always invoked by call sites
}

func (g *gatewayClient) CreateCardPayout(ctx context.Context, req *mntxv1.CardPayoutRequest) (*mntxv1.CardPayoutResponse, error) {
	ctx, cancel := g.callContext(ctx, "CreateCardPayout")
	defer cancel()
	operation, err := operationFromCardPayout(req)
	if err != nil {
		return nil, err
	}
	resp, err := g.client.SubmitOperation(ctx, &connectorv1.SubmitOperationRequest{Operation: operation})
	if err != nil {
		return nil, err
	}
	if resp.GetReceipt() != nil && resp.GetReceipt().GetError() != nil {
		return nil, connectorError(resp.GetReceipt().GetError())
	}
	return &mntxv1.CardPayoutResponse{Payout: payoutFromReceipt(req.GetPayoutId(), req.GetOperationRef(), req.GetParentPaymentRef(), resp.GetReceipt())}, nil
}

func (g *gatewayClient) CreateCardTokenPayout(ctx context.Context, req *mntxv1.CardTokenPayoutRequest) (*mntxv1.CardTokenPayoutResponse, error) {
	ctx, cancel := g.callContext(ctx, "CreateCardTokenPayout")
	defer cancel()
	operation, err := operationFromTokenPayout(req)
	if err != nil {
		return nil, err
	}
	resp, err := g.client.SubmitOperation(ctx, &connectorv1.SubmitOperationRequest{Operation: operation})
	if err != nil {
		return nil, err
	}
	if resp.GetReceipt() != nil && resp.GetReceipt().GetError() != nil {
		return nil, connectorError(resp.GetReceipt().GetError())
	}
	return &mntxv1.CardTokenPayoutResponse{Payout: payoutFromReceipt(req.GetPayoutId(), req.GetOperationRef(), req.GetParentPaymentRef(), resp.GetReceipt())}, nil
}

func (g *gatewayClient) GetCardPayoutStatus(ctx context.Context, req *mntxv1.GetCardPayoutStatusRequest) (*mntxv1.GetCardPayoutStatusResponse, error) {
	ctx, cancel := g.callContext(ctx, "GetCardPayoutStatus")
	defer cancel()
	if req == nil || strings.TrimSpace(req.GetPayoutId()) == "" {
		return nil, merrors.InvalidArgument("mcards: payout_id is required")
	}
	resp, err := g.client.GetOperation(ctx, &connectorv1.GetOperationRequest{OperationId: strings.TrimSpace(req.GetPayoutId())})
	if err != nil {
		return nil, err
	}
	return &mntxv1.GetCardPayoutStatusResponse{Payout: payoutFromOperation(resp.GetOperation())}, nil
}

func (g *gatewayClient) ListGatewayInstances(ctx context.Context, req *mntxv1.ListGatewayInstancesRequest) (*mntxv1.ListGatewayInstancesResponse, error) {
	return nil, merrors.NotImplemented("mcards: ListGatewayInstances not supported via connector")
}

func operationFromCardPayout(req *mntxv1.CardPayoutRequest) (*connectorv1.Operation, error) {
	if req == nil {
		return nil, merrors.InvalidArgument("mcards: request is required")
	}
	params := payoutParamsFromCard(req)
	money := moneyFromMinor(req.GetAmountMinor(), req.GetCurrency())
	operationRef := fallbackNonEmpty(req.GetOperationRef(), req.GetPayoutId())
	idempotencyKey := fallbackNonEmpty(req.GetIdempotencyKey(), operationRef)
	op := &connectorv1.Operation{
		Type:           connectorv1.OperationType_PAYOUT,
		IdempotencyKey: idempotencyKey,
		OperationRef:   operationRef,
		IntentRef:      strings.TrimSpace(req.GetIntentRef()),
		Money:          money,
		Params:         structFromMap(params),
	}
	setOperationRolesFromMetadata(op, req.GetMetadata())
	return op, nil
}

func operationFromTokenPayout(req *mntxv1.CardTokenPayoutRequest) (*connectorv1.Operation, error) {
	if req == nil {
		return nil, merrors.InvalidArgument("mcards: request is required")
	}
	params := payoutParamsFromToken(req)
	money := moneyFromMinor(req.GetAmountMinor(), req.GetCurrency())
	operationRef := fallbackNonEmpty(req.GetOperationRef(), req.GetPayoutId())
	idempotencyKey := fallbackNonEmpty(req.GetIdempotencyKey(), operationRef)
	op := &connectorv1.Operation{
		Type:           connectorv1.OperationType_PAYOUT,
		IdempotencyKey: idempotencyKey,
		OperationRef:   operationRef,
		IntentRef:      strings.TrimSpace(req.GetIntentRef()),
		Money:          money,
		Params:         structFromMap(params),
	}
	setOperationRolesFromMetadata(op, req.GetMetadata())
	return op, nil
}

func setOperationRolesFromMetadata(op *connectorv1.Operation, metadata map[string]string) {
	if op == nil || len(metadata) == 0 {
		return
	}
	if raw := strings.TrimSpace(metadata[account_role.MetadataKeyFromRole]); raw != "" {
		if role, ok := account_role.Parse(raw); ok && role != "" {
			op.FromRole = account_role.ToProto(role)
		}
	}
	if raw := strings.TrimSpace(metadata[account_role.MetadataKeyToRole]); raw != "" {
		if role, ok := account_role.Parse(raw); ok && role != "" {
			op.ToRole = account_role.ToProto(role)
		}
	}
}

func payoutParamsFromCard(req *mntxv1.CardPayoutRequest) map[string]interface{} {
	metadata := sanitizeMetadata(req.GetMetadata())
	params := map[string]interface{}{
		"project_id":           req.GetProjectId(),
		"parent_payment_ref":   strings.TrimSpace(req.GetParentPaymentRef()),
		"comment":              strings.TrimSpace(req.GetComment()),
		"customer_id":          strings.TrimSpace(req.GetCustomerId()),
		"customer_first_name":  strings.TrimSpace(req.GetCustomerFirstName()),
		"customer_middle_name": strings.TrimSpace(req.GetCustomerMiddleName()),
		"customer_last_name":   strings.TrimSpace(req.GetCustomerLastName()),
		"customer_ip":          strings.TrimSpace(req.GetCustomerIp()),
		"customer_zip":         strings.TrimSpace(req.GetCustomerZip()),
		"customer_country":     strings.TrimSpace(req.GetCustomerCountry()),
		"customer_state":       strings.TrimSpace(req.GetCustomerState()),
		"customer_city":        strings.TrimSpace(req.GetCustomerCity()),
		"customer_address":     strings.TrimSpace(req.GetCustomerAddress()),
		"amount_minor":         req.GetAmountMinor(),
		"currency":             strings.TrimSpace(req.GetCurrency()),
		"card_pan":             strings.TrimSpace(req.GetCardPan()),
		"card_exp_year":        req.GetCardExpYear(),
		"card_exp_month":       req.GetCardExpMonth(),
		"card_holder":          strings.TrimSpace(req.GetCardHolder()),
	}
	if len(metadata) > 0 {
		params["metadata"] = mapStringToInterface(metadata)
	}
	return params
}

func payoutParamsFromToken(req *mntxv1.CardTokenPayoutRequest) map[string]interface{} {
	metadata := sanitizeMetadata(req.GetMetadata())
	params := map[string]interface{}{
		"project_id":           req.GetProjectId(),
		"parent_payment_ref":   strings.TrimSpace(req.GetParentPaymentRef()),
		"comment":              strings.TrimSpace(req.GetComment()),
		"customer_id":          strings.TrimSpace(req.GetCustomerId()),
		"customer_first_name":  strings.TrimSpace(req.GetCustomerFirstName()),
		"customer_middle_name": strings.TrimSpace(req.GetCustomerMiddleName()),
		"customer_last_name":   strings.TrimSpace(req.GetCustomerLastName()),
		"customer_ip":          strings.TrimSpace(req.GetCustomerIp()),
		"customer_zip":         strings.TrimSpace(req.GetCustomerZip()),
		"customer_country":     strings.TrimSpace(req.GetCustomerCountry()),
		"customer_state":       strings.TrimSpace(req.GetCustomerState()),
		"customer_city":        strings.TrimSpace(req.GetCustomerCity()),
		"customer_address":     strings.TrimSpace(req.GetCustomerAddress()),
		"amount_minor":         req.GetAmountMinor(),
		"currency":             strings.TrimSpace(req.GetCurrency()),
		"card_token":           strings.TrimSpace(req.GetCardToken()),
		"card_holder":          strings.TrimSpace(req.GetCardHolder()),
		"masked_pan":           strings.TrimSpace(req.GetMaskedPan()),
	}
	if len(metadata) > 0 {
		params["metadata"] = mapStringToInterface(metadata)
	}
	return params
}

func moneyFromMinor(amount int64, currency string) *moneyv1.Money {
	if amount <= 0 {
		return nil
	}
	dec := decimal.NewFromInt(amount).Div(decimal.NewFromInt(100))
	return &moneyv1.Money{
		Amount:   dec.StringFixed(2),
		Currency: strings.ToUpper(strings.TrimSpace(currency)),
	}
}

func payoutFromReceipt(payoutID, operationRef, parentPaymentRef string, receipt *connectorv1.OperationReceipt) *mntxv1.CardPayoutState {
	state := &mntxv1.CardPayoutState{
		PayoutId:         fallbackNonEmpty(operationRef, payoutID),
		ParentPaymentRef: strings.TrimSpace(parentPaymentRef),
	}
	if receipt == nil {
		return state
	}
	if opID := strings.TrimSpace(receipt.GetOperationId()); opID != "" {
		state.PayoutId = opID
	}
	state.Status = payoutStatusFromOperation(receipt.GetStatus())
	state.ProviderPaymentId = strings.TrimSpace(receipt.GetProviderRef())
	return state
}

func fallbackNonEmpty(values ...string) string {
	for _, value := range values {
		clean := strings.TrimSpace(value)
		if clean != "" {
			return clean
		}
	}
	return ""
}

func sanitizeMetadata(source map[string]string) map[string]string {
	if len(source) == 0 {
		return nil
	}
	out := map[string]string{}
	for key, value := range source {
		k := strings.TrimSpace(key)
		if k == "" {
			continue
		}
		if strings.EqualFold(k, "comment") {
			continue
		}
		out[k] = strings.TrimSpace(value)
	}
	if len(out) == 0 {
		return nil
	}
	return out
}

func payoutFromOperation(op *connectorv1.Operation) *mntxv1.CardPayoutState {
	if op == nil {
		return nil
	}
	state := &mntxv1.CardPayoutState{
		PayoutId:          strings.TrimSpace(op.GetOperationId()),
		Status:            payoutStatusFromOperation(op.GetStatus()),
		ProviderPaymentId: strings.TrimSpace(op.GetProviderRef()),
	}
	if money := op.GetMoney(); money != nil {
		state.Currency = strings.TrimSpace(money.GetCurrency())
		state.AmountMinor = minorFromMoney(money)
	}
	if params := op.GetParams(); params != nil {
		if field, ok := params.GetFields()["comment"]; ok {
			state.Comment = strings.TrimSpace(field.GetStringValue())
		}
	}
	return state
}

func minorFromMoney(m *moneyv1.Money) int64 {
	if m == nil {
		return 0
	}
	amount := strings.TrimSpace(m.GetAmount())
	if amount == "" {
		return 0
	}
	dec, err := decimal.NewFromString(amount)
	if err != nil {
		return 0
	}
	return dec.Mul(decimal.NewFromInt(100)).IntPart()
}

func payoutStatusFromOperation(status connectorv1.OperationStatus) mntxv1.PayoutStatus {
	switch status {

	case connectorv1.OperationStatus_OPERATION_CREATED:
		return mntxv1.PayoutStatus_PAYOUT_STATUS_CREATED

	case connectorv1.OperationStatus_OPERATION_WAITING:
		return mntxv1.PayoutStatus_PAYOUT_STATUS_WAITING

	case connectorv1.OperationStatus_OPERATION_SUCCESS:
		return mntxv1.PayoutStatus_PAYOUT_STATUS_SUCCESS

	case connectorv1.OperationStatus_OPERATION_FAILED:
		return mntxv1.PayoutStatus_PAYOUT_STATUS_FAILED

	case connectorv1.OperationStatus_OPERATION_CANCELLED:
		return mntxv1.PayoutStatus_PAYOUT_STATUS_CANCELLED

	default:
		return mntxv1.PayoutStatus_PAYOUT_STATUS_UNSPECIFIED
	}
}

func connectorError(err *connectorv1.ConnectorError) error {
	if err == nil {
		return nil
	}
	msg := strings.TrimSpace(err.GetMessage())
	switch err.GetCode() {
	case connectorv1.ErrorCode_INVALID_PARAMS:
		return merrors.InvalidArgument(msg)
	case connectorv1.ErrorCode_NOT_FOUND:
		return merrors.NoData(msg)
	case connectorv1.ErrorCode_UNSUPPORTED_OPERATION, connectorv1.ErrorCode_UNSUPPORTED_ACCOUNT_KIND:
		return merrors.NotImplemented(msg)
	case connectorv1.ErrorCode_RATE_LIMITED, connectorv1.ErrorCode_TEMPORARY_UNAVAILABLE:
		return merrors.Internal(msg)
	default:
		return merrors.Internal(msg)
	}
}

func structFromMap(data map[string]interface{}) *structpb.Struct {
	if len(data) == 0 {
		return nil
	}
	result, err := structpb.NewStruct(data)
	if err != nil {
		return nil
	}
	return result
}

func mapStringToInterface(input map[string]string) map[string]interface{} {
	if len(input) == 0 {
		return nil
	}
	out := make(map[string]interface{}, len(input))
	for k, v := range input {
		out[k] = v
	}
	return out
}
