package gateway

import (
	"context"
	"errors"
	"strings"
	"time"

	"github.com/ethereum/go-ethereum/core/types"
	"github.com/tech/sendico/chain/gateway/storage/model"
	"github.com/tech/sendico/pkg/merrors"
	"go.uber.org/zap"
)

func (s *Service) launchTransferExecution(transferRef, sourceWalletRef string, network Network) {
	if s.executor == nil {
		return
	}

	go func(ref, walletRef string, net Network) {
		ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute)
		defer cancel()

		if err := s.executeTransfer(ctx, ref, walletRef, net); err != nil {
			s.logger.Error("failed to execute transfer", zap.String("transfer_ref", ref), zap.Error(err))
		}
	}(transferRef, sourceWalletRef, network)
}

func (s *Service) executeTransfer(ctx context.Context, transferRef, sourceWalletRef string, network Network) error {
	transfer, err := s.storage.Transfers().Get(ctx, transferRef)
	if err != nil {
		return err
	}

	sourceWallet, err := s.storage.Wallets().Get(ctx, sourceWalletRef)
	if err != nil {
		return err
	}

	if _, err := s.storage.Transfers().UpdateStatus(ctx, transferRef, model.TransferStatusSigning, "", ""); err != nil {
		s.logger.Warn("failed to update transfer status to signing", zap.String("transfer_ref", transferRef), zap.Error(err))
	}

	destinationAddress, err := s.destinationAddress(ctx, transfer.Destination)
	if err != nil {
		_, _ = s.storage.Transfers().UpdateStatus(ctx, transferRef, model.TransferStatusFailed, err.Error(), "")
		return err
	}

	txHash, err := s.executor.SubmitTransfer(ctx, transfer, sourceWallet, destinationAddress, network)
	if err != nil {
		_, _ = s.storage.Transfers().UpdateStatus(ctx, transferRef, model.TransferStatusFailed, err.Error(), "")
		return err
	}

	if _, err := s.storage.Transfers().UpdateStatus(ctx, transferRef, model.TransferStatusSubmitted, "", txHash); err != nil {
		s.logger.Warn("failed to update transfer status to submitted", zap.String("transfer_ref", transferRef), zap.Error(err))
	}

	receiptCtx, cancel := context.WithTimeout(ctx, 10*time.Minute)
	defer cancel()
	receipt, err := s.executor.AwaitConfirmation(receiptCtx, network, txHash)
	if err != nil {
		if !errors.Is(err, context.DeadlineExceeded) && !errors.Is(err, context.Canceled) {
			s.logger.Warn("failed to await transfer confirmation", zap.String("transfer_ref", transferRef), zap.Error(err))
		}
		return err
	}

	if receipt != nil && receipt.Status == types.ReceiptStatusSuccessful {
		if _, err := s.storage.Transfers().UpdateStatus(ctx, transferRef, model.TransferStatusConfirmed, "", txHash); err != nil {
			s.logger.Warn("failed to update transfer status to confirmed", zap.String("transfer_ref", transferRef), zap.Error(err))
		}
		return nil
	}

	if _, err := s.storage.Transfers().UpdateStatus(ctx, transferRef, model.TransferStatusFailed, "transaction reverted", txHash); err != nil {
		s.logger.Warn("failed to update transfer status to failed", zap.String("transfer_ref", transferRef), zap.Error(err))
	}
	return nil
}

func (s *Service) destinationAddress(ctx context.Context, dest model.TransferDestination) (string, error) {
	if ref := strings.TrimSpace(dest.ManagedWalletRef); ref != "" {
		wallet, err := s.storage.Wallets().Get(ctx, ref)
		if err != nil {
			return "", err
		}
		if strings.TrimSpace(wallet.DepositAddress) == "" {
			return "", merrors.Internal("destination wallet missing deposit address")
		}
		return wallet.DepositAddress, nil
	}
	if addr := strings.TrimSpace(dest.ExternalAddress); addr != "" {
		return strings.ToLower(addr), nil
	}
	return "", merrors.InvalidArgument("transfer destination address not resolved")
}
