package gateway

import (
	"context"
	"errors"
	"strings"

	"github.com/shopspring/decimal"
	"github.com/tech/sendico/chain/gateway/storage/model"
	"github.com/tech/sendico/pkg/api/routers/gsresponse"
	"github.com/tech/sendico/pkg/merrors"
	"github.com/tech/sendico/pkg/mservice"
	gatewayv1 "github.com/tech/sendico/pkg/proto/chain/gateway/v1"
	moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
	paginationv1 "github.com/tech/sendico/pkg/proto/common/pagination/v1"
	"go.uber.org/zap"
	"google.golang.org/protobuf/types/known/timestamppb"
)

func (s *Service) submitTransferHandler(ctx context.Context, req *gatewayv1.SubmitTransferRequest) gsresponse.Responder[gatewayv1.SubmitTransferResponse] {
	if err := s.ensureRepository(ctx); err != nil {
		return gsresponse.Unavailable[gatewayv1.SubmitTransferResponse](s.logger, mservice.ChainGateway, err)
	}
	if req == nil {
		return gsresponse.InvalidArgument[gatewayv1.SubmitTransferResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("nil request"))
	}

	idempotencyKey := strings.TrimSpace(req.GetIdempotencyKey())
	if idempotencyKey == "" {
		return gsresponse.InvalidArgument[gatewayv1.SubmitTransferResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("idempotency_key is required"))
	}
	organizationRef := strings.TrimSpace(req.GetOrganizationRef())
	if organizationRef == "" {
		return gsresponse.InvalidArgument[gatewayv1.SubmitTransferResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("organization_ref is required"))
	}
	sourceWalletRef := strings.TrimSpace(req.GetSourceWalletRef())
	if sourceWalletRef == "" {
		return gsresponse.InvalidArgument[gatewayv1.SubmitTransferResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("source_wallet_ref is required"))
	}
	amount := req.GetAmount()
	if amount == nil {
		return gsresponse.InvalidArgument[gatewayv1.SubmitTransferResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("amount is required"))
	}
	amountCurrency := strings.ToUpper(strings.TrimSpace(amount.GetCurrency()))
	if amountCurrency == "" {
		return gsresponse.InvalidArgument[gatewayv1.SubmitTransferResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("amount.currency is required"))
	}
	amountValue := strings.TrimSpace(amount.GetAmount())
	if amountValue == "" {
		return gsresponse.InvalidArgument[gatewayv1.SubmitTransferResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("amount.amount is required"))
	}

	sourceWallet, err := s.storage.Wallets().Get(ctx, sourceWalletRef)
	if err != nil {
		if errors.Is(err, merrors.ErrNoData) {
			return gsresponse.NotFound[gatewayv1.SubmitTransferResponse](s.logger, mservice.ChainGateway, err)
		}
		return gsresponse.Auto[gatewayv1.SubmitTransferResponse](s.logger, mservice.ChainGateway, err)
	}
	if !strings.EqualFold(sourceWallet.OrganizationRef, organizationRef) {
		return gsresponse.InvalidArgument[gatewayv1.SubmitTransferResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("organization_ref mismatch with wallet"))
	}
	networkKey := strings.ToLower(strings.TrimSpace(sourceWallet.Network))
	networkCfg, ok := s.networks[networkKey]
	if !ok {
		return gsresponse.InvalidArgument[gatewayv1.SubmitTransferResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("unsupported chain for wallet"))
	}

	destination, err := s.resolveDestination(ctx, req.GetDestination(), sourceWallet)
	if err != nil {
		if errors.Is(err, merrors.ErrNoData) {
			return gsresponse.NotFound[gatewayv1.SubmitTransferResponse](s.logger, mservice.ChainGateway, err)
		}
		return gsresponse.InvalidArgument[gatewayv1.SubmitTransferResponse](s.logger, mservice.ChainGateway, err)
	}

	fees, feeSum, err := convertFees(req.GetFees(), amountCurrency)
	if err != nil {
		return gsresponse.InvalidArgument[gatewayv1.SubmitTransferResponse](s.logger, mservice.ChainGateway, err)
	}
	amountDec, err := decimal.NewFromString(amountValue)
	if err != nil {
		return gsresponse.InvalidArgument[gatewayv1.SubmitTransferResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("invalid amount"))
	}
	netDec := amountDec.Sub(feeSum)
	if netDec.IsNegative() {
		return gsresponse.InvalidArgument[gatewayv1.SubmitTransferResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("fees exceed amount"))
	}

	netAmount := cloneMoney(amount)
	netAmount.Amount = netDec.String()

	transfer := &model.Transfer{
		IdempotencyKey:  idempotencyKey,
		TransferRef:     generateTransferRef(),
		OrganizationRef: organizationRef,
		SourceWalletRef: sourceWalletRef,
		Destination:     destination,
		Network:         sourceWallet.Network,
		TokenSymbol:     sourceWallet.TokenSymbol,
		ContractAddress: sourceWallet.ContractAddress,
		RequestedAmount: cloneMoney(amount),
		NetAmount:       netAmount,
		Fees:            fees,
		Status:          model.TransferStatusPending,
		ClientReference: strings.TrimSpace(req.GetClientReference()),
		LastStatusAt:    s.clock.Now().UTC(),
	}

	saved, err := s.storage.Transfers().Create(ctx, transfer)
	if err != nil {
		if errors.Is(err, merrors.ErrDataConflict) {
			s.logger.Debug("transfer already exists", zap.String("transfer_ref", transfer.TransferRef), zap.String("idempotency_key", idempotencyKey))
			return gsresponse.Success(&gatewayv1.SubmitTransferResponse{Transfer: s.toProtoTransfer(saved)})
		}
		return gsresponse.Auto[gatewayv1.SubmitTransferResponse](s.logger, mservice.ChainGateway, err)
	}

	if s.executor != nil {
		s.launchTransferExecution(saved.TransferRef, sourceWalletRef, networkCfg)
	}

	return gsresponse.Success(&gatewayv1.SubmitTransferResponse{Transfer: s.toProtoTransfer(saved)})
}

func (s *Service) getTransferHandler(ctx context.Context, req *gatewayv1.GetTransferRequest) gsresponse.Responder[gatewayv1.GetTransferResponse] {
	if err := s.ensureRepository(ctx); err != nil {
		return gsresponse.Unavailable[gatewayv1.GetTransferResponse](s.logger, mservice.ChainGateway, err)
	}
	if req == nil {
		return gsresponse.InvalidArgument[gatewayv1.GetTransferResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("nil request"))
	}
	transferRef := strings.TrimSpace(req.GetTransferRef())
	if transferRef == "" {
		return gsresponse.InvalidArgument[gatewayv1.GetTransferResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("transfer_ref is required"))
	}
	transfer, err := s.storage.Transfers().Get(ctx, transferRef)
	if err != nil {
		if errors.Is(err, merrors.ErrNoData) {
			return gsresponse.NotFound[gatewayv1.GetTransferResponse](s.logger, mservice.ChainGateway, err)
		}
		return gsresponse.Auto[gatewayv1.GetTransferResponse](s.logger, mservice.ChainGateway, err)
	}
	return gsresponse.Success(&gatewayv1.GetTransferResponse{Transfer: s.toProtoTransfer(transfer)})
}

func (s *Service) listTransfersHandler(ctx context.Context, req *gatewayv1.ListTransfersRequest) gsresponse.Responder[gatewayv1.ListTransfersResponse] {
	if err := s.ensureRepository(ctx); err != nil {
		return gsresponse.Unavailable[gatewayv1.ListTransfersResponse](s.logger, mservice.ChainGateway, err)
	}
	filter := model.TransferFilter{}
	if req != nil {
		filter.SourceWalletRef = strings.TrimSpace(req.GetSourceWalletRef())
		filter.DestinationWalletRef = strings.TrimSpace(req.GetDestinationWalletRef())
		if status := transferStatusToModel(req.GetStatus()); status != "" {
			filter.Status = status
		}
		if page := req.GetPage(); page != nil {
			filter.Cursor = strings.TrimSpace(page.GetCursor())
			filter.Limit = page.GetLimit()
		}
	}

	result, err := s.storage.Transfers().List(ctx, filter)
	if err != nil {
		return gsresponse.Auto[gatewayv1.ListTransfersResponse](s.logger, mservice.ChainGateway, err)
	}

	protoTransfers := make([]*gatewayv1.Transfer, 0, len(result.Items))
	for _, transfer := range result.Items {
		protoTransfers = append(protoTransfers, s.toProtoTransfer(transfer))
	}

	resp := &gatewayv1.ListTransfersResponse{
		Transfers: protoTransfers,
		Page:      &paginationv1.CursorPageResponse{NextCursor: result.NextCursor},
	}
	return gsresponse.Success(resp)
}

func (s *Service) estimateTransferFeeHandler(ctx context.Context, req *gatewayv1.EstimateTransferFeeRequest) gsresponse.Responder[gatewayv1.EstimateTransferFeeResponse] {
	if err := s.ensureRepository(ctx); err != nil {
		return gsresponse.Unavailable[gatewayv1.EstimateTransferFeeResponse](s.logger, mservice.ChainGateway, err)
	}
	if req == nil || req.GetAmount() == nil {
		return gsresponse.InvalidArgument[gatewayv1.EstimateTransferFeeResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("amount is required"))
	}
	currency := req.GetAmount().GetCurrency()
	fee := &moneyv1.Money{
		Currency: currency,
		Amount:   "0",
	}
	resp := &gatewayv1.EstimateTransferFeeResponse{
		NetworkFee:        fee,
		EstimationContext: "not_implemented",
	}
	return gsresponse.Success(resp)
}

func (s *Service) toProtoTransfer(transfer *model.Transfer) *gatewayv1.Transfer {
	if transfer == nil {
		return nil
	}
	destination := &gatewayv1.TransferDestination{}
	if transfer.Destination.ManagedWalletRef != "" {
		destination.Destination = &gatewayv1.TransferDestination_ManagedWalletRef{ManagedWalletRef: transfer.Destination.ManagedWalletRef}
	} else if transfer.Destination.ExternalAddress != "" {
		destination.Destination = &gatewayv1.TransferDestination_ExternalAddress{ExternalAddress: transfer.Destination.ExternalAddress}
	}
	destination.Memo = transfer.Destination.Memo

	protoFees := make([]*gatewayv1.ServiceFeeBreakdown, 0, len(transfer.Fees))
	for _, fee := range transfer.Fees {
		protoFees = append(protoFees, &gatewayv1.ServiceFeeBreakdown{
			FeeCode:     fee.FeeCode,
			Amount:      cloneMoney(fee.Amount),
			Description: fee.Description,
		})
	}

	asset := &gatewayv1.Asset{
		Chain:           chainEnumFromName(transfer.Network),
		TokenSymbol:     transfer.TokenSymbol,
		ContractAddress: transfer.ContractAddress,
	}

	return &gatewayv1.Transfer{
		TransferRef:     transfer.TransferRef,
		IdempotencyKey:  transfer.IdempotencyKey,
		OrganizationRef: transfer.OrganizationRef,
		SourceWalletRef: transfer.SourceWalletRef,
		Destination:     destination,
		Asset:           asset,
		RequestedAmount: cloneMoney(transfer.RequestedAmount),
		NetAmount:       cloneMoney(transfer.NetAmount),
		Fees:            protoFees,
		Status:          transferStatusToProto(transfer.Status),
		TransactionHash: transfer.TxHash,
		FailureReason:   transfer.FailureReason,
		CreatedAt:       timestamppb.New(transfer.CreatedAt.UTC()),
		UpdatedAt:       timestamppb.New(transfer.UpdatedAt.UTC()),
	}
}

func (s *Service) resolveDestination(ctx context.Context, dest *gatewayv1.TransferDestination, source *model.ManagedWallet) (model.TransferDestination, error) {
	if dest == nil {
		return model.TransferDestination{}, merrors.InvalidArgument("destination is required")
	}
	managedRef := strings.TrimSpace(dest.GetManagedWalletRef())
	external := strings.TrimSpace(dest.GetExternalAddress())
	if managedRef != "" && external != "" {
		return model.TransferDestination{}, merrors.InvalidArgument("destination must be managed_wallet_ref or external_address")
	}
	if managedRef != "" {
		wallet, err := s.storage.Wallets().Get(ctx, managedRef)
		if err != nil {
			return model.TransferDestination{}, err
		}
		if !strings.EqualFold(wallet.Network, source.Network) {
			return model.TransferDestination{}, merrors.InvalidArgument("destination wallet network mismatch")
		}
		if strings.TrimSpace(wallet.DepositAddress) == "" {
			return model.TransferDestination{}, merrors.InvalidArgument("destination wallet missing deposit address")
		}
		return model.TransferDestination{
			ManagedWalletRef: wallet.WalletRef,
			Memo:             strings.TrimSpace(dest.GetMemo()),
		}, nil
	}
	if external == "" {
		return model.TransferDestination{}, merrors.InvalidArgument("destination is required")
	}
	return model.TransferDestination{
		ExternalAddress: strings.ToLower(external),
		Memo:            strings.TrimSpace(dest.GetMemo()),
	}, nil
}

func convertFees(fees []*gatewayv1.ServiceFeeBreakdown, currency string) ([]model.ServiceFee, decimal.Decimal, error) {
	result := make([]model.ServiceFee, 0, len(fees))
	sum := decimal.NewFromInt(0)
	for _, fee := range fees {
		if fee == nil || fee.GetAmount() == nil {
			return nil, decimal.Decimal{}, merrors.InvalidArgument("fee amount is required")
		}
		amtCurrency := strings.ToUpper(strings.TrimSpace(fee.GetAmount().GetCurrency()))
		if amtCurrency != strings.ToUpper(currency) {
			return nil, decimal.Decimal{}, merrors.InvalidArgument("fee currency mismatch")
		}
		amtValue := strings.TrimSpace(fee.GetAmount().GetAmount())
		if amtValue == "" {
			return nil, decimal.Decimal{}, merrors.InvalidArgument("fee amount is required")
		}
		dec, err := decimal.NewFromString(amtValue)
		if err != nil {
			return nil, decimal.Decimal{}, merrors.InvalidArgument("invalid fee amount")
		}
		if dec.IsNegative() {
			return nil, decimal.Decimal{}, merrors.InvalidArgument("fee amount must be non-negative")
		}
		sum = sum.Add(dec)
		result = append(result, model.ServiceFee{
			FeeCode:     strings.TrimSpace(fee.GetFeeCode()),
			Amount:      cloneMoney(fee.GetAmount()),
			Description: strings.TrimSpace(fee.GetDescription()),
		})
	}
	return result, sum, nil
}
