package gateway

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

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

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

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

		if err := s.executeTransfer(ctx, ref, walletRef, net); err != nil {
			s.logger.Warn("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 shared.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.updateTransferStatus(ctx, transferRef, model.TransferStatusProcessing, "", ""); err != nil {
		s.logger.Warn("Failed to update transfer status to signing", zap.String("transfer_ref", transferRef), zap.Error(err))
	}

	driverDeps := s.driverDeps()
	chainDriver, err := s.driverForNetwork(network.Name)
	if err != nil {
		_ = s.updateTransferStatus(ctx, transferRef, model.TransferStatusFailed, err.Error(), "")
		return err
	}

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

	sourceAddress, err := chainDriver.NormalizeAddress(sourceWallet.DepositAddress)
	if err != nil {
		_ = s.updateTransferStatus(ctx, transferRef, model.TransferStatusFailed, err.Error(), "")
		return err
	}
	if chainDriver.Name() == "tron" && sourceAddress == destinationAddress {
		s.logger.Info("Self transfer detected; skipping submission",
			zap.String("transfer_ref", transferRef),
			zap.String("wallet_ref", sourceWalletRef),
			zap.String("network", string(network.Name)),
		)
		if err := s.updateTransferStatus(ctx, transferRef, model.TransferStatusSuccess, "", ""); err != nil {
			s.logger.Warn("Failed to update transfer status to confirmed", zap.String("transfer_ref", transferRef), zap.Error(err))
		}
		return nil
	}

	txHash, err := chainDriver.SubmitTransfer(ctx, driverDeps, network, transfer, sourceWallet, destinationAddress)
	if err != nil {
		s.logger.Warn("Failed to submit transfer", zap.String("transfer_ref", transferRef), zap.Error(err))
		if e := s.updateTransferStatus(ctx, transferRef, model.TransferStatusFailed, err.Error(), ""); e != nil {
			s.logger.Warn("Failed to update transfer status to failed", zap.String("transfer_ref", transferRef), zap.Error(e))
		}
		return err
	}

	if err := s.updateTransferStatus(ctx, transferRef, model.TransferStatusWaiting, "", 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 := chainDriver.AwaitConfirmation(receiptCtx, driverDeps, 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
	}

	failureReason := ""
	pStatus := model.TransferStatusSuccess
	if receipt != nil && receipt.Status != types.ReceiptStatusSuccessful {
		failureReason = "transaction reverted"
		pStatus = model.TransferStatusFailed
	}
	if err := s.updateTransferStatus(ctx, transferRef, pStatus, failureReason, txHash); err != nil {
		s.logger.Warn("Failed to update transfer status", zap.Error(err),
			zap.String("transfer_ref", transferRef), zap.String("status", string(pStatus)))
	}
	return nil
}

func (s *Service) destinationAddress(ctx context.Context, chainDriver driver.Driver, 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 chainDriver.NormalizeAddress(wallet.DepositAddress)
	}
	if addr := strings.TrimSpace(dest.ExternalAddress); addr != "" {
		return chainDriver.NormalizeAddress(addr)
	}
	return "", merrors.InvalidArgument("transfer destination address not resolved")
}

func (s *Service) driverDeps() driver.Deps {
	return driver.Deps{
		Logger:     s.logger.Named("driver"),
		Registry:   s.networkRegistry,
		KeyManager: s.keyManager,
		RPCTimeout: s.settings.rpcTimeout(),
	}
}

func (s *Service) driverForNetwork(network pmodel.ChainNetwork) (driver.Driver, error) {
	if s.drivers == nil {
		return nil, merrors.Internal("chain drivers not configured")
	}
	return s.drivers.Driver(network)
}
