package client

import (
	"context"
	"strings"

	"github.com/tech/sendico/pkg/discovery"
	"github.com/tech/sendico/pkg/merrors"
	"github.com/tech/sendico/pkg/model/account_role"
	"github.com/tech/sendico/pkg/payments/rail"
	moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
	chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
)

// RailGatewayConfig defines metadata for the rail gateway adapter.
type RailGatewayConfig struct {
	Rail         string
	Network      string
	Capabilities rail.RailCapabilities
}

type chainRailGateway struct {
	client       Client
	rail         string
	network      string
	capabilities rail.RailCapabilities
}

// NewRailGateway wraps a chain gateway client into a rail gateway adapter.
func NewRailGateway(client Client, cfg RailGatewayConfig) rail.RailGateway {
	railName := discovery.NormalizeRail(cfg.Rail)
	if railName == "" {
		railName = discovery.RailCrypto
	}
	return &chainRailGateway{
		client:       client,
		rail:         railName,
		network:      strings.ToUpper(strings.TrimSpace(cfg.Network)),
		capabilities: cfg.Capabilities,
	}
}

func (g *chainRailGateway) Rail() string {
	return g.rail
}

func (g *chainRailGateway) Network() string {
	return g.network
}

func (g *chainRailGateway) Capabilities() rail.RailCapabilities {
	return g.capabilities
}

func (g *chainRailGateway) Send(ctx context.Context, req rail.TransferRequest) (rail.RailResult, error) {
	if g.client == nil {
		return rail.RailResult{}, merrors.Internal("chain gateway: client is required")
	}
	orgRef := strings.TrimSpace(req.OrganizationRef)
	if orgRef == "" {
		return rail.RailResult{}, merrors.InvalidArgument("chain gateway: organization_ref is required")
	}
	source := strings.TrimSpace(req.FromAccountID)
	if source == "" {
		return rail.RailResult{}, merrors.InvalidArgument("chain gateway: from_account_id is required")
	}
	destRef := strings.TrimSpace(req.ToAccountID)
	if destRef == "" {
		return rail.RailResult{}, merrors.InvalidArgument("chain gateway: to_account_id is required")
	}
	currency := strings.TrimSpace(req.Currency)
	amountValue := strings.TrimSpace(req.Amount)
	if currency == "" || amountValue == "" {
		return rail.RailResult{}, merrors.InvalidArgument("chain gateway: amount is required")
	}
	reqNetwork := strings.TrimSpace(req.Network)
	if g.network != "" && reqNetwork != "" && !strings.EqualFold(g.network, reqNetwork) {
		return rail.RailResult{}, merrors.InvalidArgument("chain gateway: network mismatch")
	}
	if strings.TrimSpace(req.IdempotencyKey) == "" {
		return rail.RailResult{}, merrors.InvalidArgument("chain gateway: idempotency_key is required")
	}

	dest, err := g.resolveDestination(ctx, destRef, strings.TrimSpace(req.DestinationMemo))
	if err != nil {
		return rail.RailResult{}, err
	}

	fees := toServiceFees(req.Fees)
	if len(fees) == 0 && req.Fee != nil {
		if amt := moneyFromRail(req.Fee); amt != nil {
			fees = []*chainv1.ServiceFeeBreakdown{{
				FeeCode: "fee",
				Amount:  amt,
			}}
		}
	}

	resp, err := g.client.SubmitTransfer(ctx, &chainv1.SubmitTransferRequest{
		IdempotencyKey:  strings.TrimSpace(req.IdempotencyKey),
		OrganizationRef: orgRef,
		SourceWalletRef: source,
		Destination:     dest,
		Amount: &moneyv1.Money{
			Currency: currency,
			Amount:   amountValue,
		},
		Fees:         fees,
		Metadata:     transferMetadataWithRoles(req.Metadata, req.FromRole, req.ToRole),
		PaymentRef:   strings.TrimSpace(req.PaymentRef),
		OperationRef: strings.TrimSpace(req.OperationRef),
		IntentRef:    strings.TrimSpace(req.OperationRef),
	})
	if err != nil {
		return rail.RailResult{}, err
	}
	if resp == nil || resp.GetTransfer() == nil {
		return rail.RailResult{}, merrors.Internal("chain gateway: missing transfer response")
	}

	transfer := resp.GetTransfer()
	return rail.RailResult{
		ReferenceID: strings.TrimSpace(transfer.GetTransferRef()),
		Status:      statusFromTransfer(transfer.GetStatus()),
		FinalAmount: railMoneyFromProto(transfer.GetNetAmount()),
	}, nil
}

func (g *chainRailGateway) Observe(ctx context.Context, referenceID string) (rail.ObserveResult, error) {
	if g.client == nil {
		return rail.ObserveResult{}, merrors.Internal("chain gateway: client is required")
	}
	ref := strings.TrimSpace(referenceID)
	if ref == "" {
		return rail.ObserveResult{}, merrors.InvalidArgument("chain gateway: reference_id is required")
	}
	resp, err := g.client.GetTransfer(ctx, &chainv1.GetTransferRequest{TransferRef: ref})
	if err != nil {
		return rail.ObserveResult{}, err
	}
	if resp == nil || resp.GetTransfer() == nil {
		return rail.ObserveResult{}, merrors.Internal("chain gateway: missing transfer response")
	}
	transfer := resp.GetTransfer()
	return rail.ObserveResult{
		ReferenceID: ref,
		Status:      statusFromTransfer(transfer.GetStatus()),
		FinalAmount: railMoneyFromProto(transfer.GetNetAmount()),
	}, nil
}

func (g *chainRailGateway) Block(ctx context.Context, req rail.BlockRequest) (rail.RailResult, error) {
	return rail.RailResult{}, merrors.NotImplemented("chain gateway: block not supported")
}

func (g *chainRailGateway) Release(ctx context.Context, req rail.ReleaseRequest) (rail.RailResult, error) {
	return rail.RailResult{}, merrors.NotImplemented("chain gateway: release not supported")
}

func (g *chainRailGateway) resolveDestination(ctx context.Context, destRef, memo string) (*chainv1.TransferDestination, error) {
	managed, err := g.isManagedWallet(ctx, destRef)
	if err != nil {
		return nil, err
	}
	if managed {
		return &chainv1.TransferDestination{
			Destination: &chainv1.TransferDestination_ManagedWalletRef{ManagedWalletRef: destRef},
		}, nil
	}
	return &chainv1.TransferDestination{
		Destination: &chainv1.TransferDestination_ExternalAddress{ExternalAddress: destRef},
		Memo:        memo,
	}, nil
}

func (g *chainRailGateway) isManagedWallet(ctx context.Context, walletRef string) (bool, error) {
	resp, err := g.client.GetManagedWallet(ctx, &chainv1.GetManagedWalletRequest{WalletRef: walletRef})
	if err != nil {
		if status.Code(err) == codes.NotFound {
			return false, nil
		}
		return false, err
	}
	if resp == nil || resp.GetWallet() == nil {
		return false, nil
	}
	return true, nil
}

func statusFromTransfer(status chainv1.TransferStatus) rail.TransferStatus {
	switch status {

	case chainv1.TransferStatus_TRANSFER_CREATED:
		return rail.TransferStatusCreated

	case chainv1.TransferStatus_TRANSFER_PROCESSING:
		return rail.TransferStatusProcessing

	case chainv1.TransferStatus_TRANSFER_WAITING:
		return rail.TransferStatusProcessing

	case chainv1.TransferStatus_TRANSFER_SUCCESS:
		return rail.TransferStatusSuccess

	case chainv1.TransferStatus_TRANSFER_FAILED:
		return rail.TransferStatusFailed

	case chainv1.TransferStatus_TRANSFER_CANCELLED:
		return rail.TransferStatusCancelled

	default:
		return rail.TransferStatusUnspecified
	}
}

func toServiceFees(fees []rail.FeeBreakdown) []*chainv1.ServiceFeeBreakdown {
	if len(fees) == 0 {
		return nil
	}
	result := make([]*chainv1.ServiceFeeBreakdown, 0, len(fees))
	for _, fee := range fees {
		amount := moneyFromRail(fee.Amount)
		if amount == nil {
			continue
		}
		result = append(result, &chainv1.ServiceFeeBreakdown{
			FeeCode:     strings.TrimSpace(fee.FeeCode),
			Amount:      amount,
			Description: strings.TrimSpace(fee.Description),
		})
	}
	if len(result) == 0 {
		return nil
	}
	return result
}

func moneyFromRail(m *rail.Money) *moneyv1.Money {
	if m == nil {
		return nil
	}
	currency := strings.TrimSpace(m.GetCurrency())
	amount := strings.TrimSpace(m.GetAmount())
	if currency == "" || amount == "" {
		return nil
	}
	return &moneyv1.Money{
		Currency: currency,
		Amount:   amount,
	}
}

func railMoneyFromProto(m *moneyv1.Money) *rail.Money {
	if m == nil {
		return nil
	}
	currency := strings.TrimSpace(m.GetCurrency())
	amount := strings.TrimSpace(m.GetAmount())
	if currency == "" || amount == "" {
		return nil
	}
	return &rail.Money{
		Currency: currency,
		Amount:   amount,
	}
}

func transferMetadataWithRoles(metadata map[string]string, fromRole, toRole account_role.AccountRole) map[string]string {
	result := cloneMetadata(metadata)
	if strings.TrimSpace(string(fromRole)) != "" {
		if result == nil {
			result = map[string]string{}
		}
		result[account_role.MetadataKeyFromRole] = strings.TrimSpace(string(fromRole))
	}
	if strings.TrimSpace(string(toRole)) != "" {
		if result == nil {
			result = map[string]string{}
		}
		result[account_role.MetadataKeyToRole] = strings.TrimSpace(string(toRole))
	}
	if len(result) == 0 {
		return nil
	}
	return result
}

func cloneMetadata(input map[string]string) map[string]string {
	if len(input) == 0 {
		return nil
	}
	result := make(map[string]string, len(input))
	for key, value := range input {
		result[key] = value
	}
	return result
}
