"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.PaymentSweeper = void 0;
const mongoose_1 = __importDefault(require("mongoose"));
const ethers_1 = require("ethers");
const tronweb_1 = require("tronweb");
const PaymentTransactionModel_js_1 = require("../database/models/PaymentTransactionModel.js");
const PaymentWalletModel_js_1 = require("../database/models/PaymentWalletModel.js");
const SweepVaultModel_js_1 = require("../database/models/SweepVaultModel.js");
const VaultGasService_js_1 = require("./VaultGasService.js");
const erc20Abi = [
    "function balanceOf(address owner) view returns (uint256)",
    "function transfer(address to, uint256 amount) returns (bool)"
];
const trc20Abi = [
    {
        constant: true,
        inputs: [{ name: "who", type: "address" }],
        name: "balanceOf",
        outputs: [{ name: "", type: "uint256" }],
        type: "function"
    },
    {
        constant: false,
        inputs: [
            { name: "_to", type: "address" },
            { name: "_value", type: "uint256" }
        ],
        name: "transfer",
        outputs: [{ name: "", type: "bool" }],
        payable: false,
        stateMutability: "nonpayable",
        type: "function"
    }
];
class PaymentSweeper {
    config;
    cryptoBox;
    logger;
    gasService;
    constructor(config, cryptoBox, logger) {
        this.config = config;
        this.cryptoBox = cryptoBox;
        this.logger = logger;
        this.gasService = new VaultGasService_js_1.VaultGasService(config, cryptoBox, logger);
    }
    isConfigured() {
        return Boolean(this.config.evmVaultAddress || this.config.tronVaultAddress);
    }
    async sweepUnsweptTransactions(vaultId) {
        const summary = {
            processed: 0,
            swept: 0,
            skipped: 0,
            failed: 0,
            totalAmount: "0",
            records: []
        };
        const vault = await this.resolveVault(vaultId);
        const hasVault = Boolean(vault);
        const hasLegacyAddresses = this.isConfigured();
        if (!hasVault && !hasLegacyAddresses) {
            this.logger.warn("Payment sweep skipped: no active SweepVault and no legacy EVM_VAULT_ADDRESS/TRON_VAULT_ADDRESS set.");
            return summary;
        }
        const unswept = await PaymentTransactionModel_js_1.PaymentTransactionModel.find({
            status: "confirmed",
            sweptAt: { $exists: false },
            manuallyConfirmedAt: { $exists: false }
        }).lean();
        const walletGroups = this.groupByWallet(unswept);
        this.logger.info(`Starting payment sweep for ${walletGroups.length} unique wallet(s).`, {
            transactionCount: unswept.length,
            vaultId: vault?._id.toString()
        });
        for (const group of walletGroups) {
            summary.processed++;
            const record = await this.sweepWallet(group.network, group.walletAddress, group.transactionIds, vault);
            summary.records.push(record);
            if (record.error) {
                summary.failed++;
                this.logger.error(`Sweep failed for ${group.network} wallet ${group.walletAddress}.`, {
                    error: record.error
                });
            }
            else if (record.txHash) {
                summary.swept++;
                summary.totalAmount = (BigInt(summary.totalAmount) + BigInt(record.amount)).toString();
                this.logger.info(`Swept ${record.amount} USDT from ${group.network} wallet ${group.walletAddress}.`, {
                    txHash: record.txHash,
                    feeTxHash: record.feeTxHash,
                    swapTxHash: record.swapTxHash
                });
            }
            else {
                summary.skipped++;
                this.logger.info(`Skipped ${group.network} wallet ${group.walletAddress}: zero balance.`);
            }
        }
        this.logger.info("Payment sweep complete.", {
            processed: summary.processed,
            swept: summary.swept,
            skipped: summary.skipped,
            failed: summary.failed,
            totalAmount: summary.totalAmount
        });
        return summary;
    }
    async sweepTransactionIds(transactionIds, vaultId) {
        const summary = {
            processed: 0,
            swept: 0,
            skipped: 0,
            failed: 0,
            totalAmount: "0",
            records: []
        };
        if (transactionIds.length === 0) {
            return summary;
        }
        const vault = await this.resolveVault(vaultId);
        const hasVault = Boolean(vault);
        const hasLegacyAddresses = this.isConfigured();
        if (!hasVault && !hasLegacyAddresses) {
            this.logger.warn("Payment sweep skipped: no active SweepVault and no legacy EVM_VAULT_ADDRESS/TRON_VAULT_ADDRESS set.");
            return summary;
        }
        const objectIds = transactionIds
            .map((id) => {
            try {
                return new mongoose_1.default.Types.ObjectId(id);
            }
            catch {
                return null;
            }
        })
            .filter((id) => id !== null);
        if (objectIds.length === 0) {
            return summary;
        }
        const selected = await PaymentTransactionModel_js_1.PaymentTransactionModel.find({
            _id: { $in: objectIds },
            status: "confirmed",
            manuallyConfirmedAt: { $exists: false }
        }).lean();
        const walletGroups = this.groupByWallet(selected);
        this.logger.info(`Starting selected payment sweep for ${walletGroups.length} unique wallet(s).`, {
            requestedCount: transactionIds.length,
            matchedCount: selected.length,
            vaultId: vault?._id.toString()
        });
        for (const group of walletGroups) {
            summary.processed++;
            const record = await this.sweepWallet(group.network, group.walletAddress, group.transactionIds, vault);
            summary.records.push(record);
            if (record.error) {
                summary.failed++;
                this.logger.error(`Sweep failed for ${group.network} wallet ${group.walletAddress}.`, {
                    error: record.error
                });
            }
            else if (record.txHash) {
                summary.swept++;
                summary.totalAmount = (BigInt(summary.totalAmount) + BigInt(record.amount)).toString();
                this.logger.info(`Swept ${record.amount} USDT from ${group.network} wallet ${group.walletAddress}.`, {
                    txHash: record.txHash,
                    feeTxHash: record.feeTxHash,
                    swapTxHash: record.swapTxHash
                });
            }
            else {
                summary.skipped++;
                this.logger.info(`Skipped ${group.network} wallet ${group.walletAddress}: zero balance.`);
            }
        }
        this.logger.info("Selected payment sweep complete.", {
            processed: summary.processed,
            swept: summary.swept,
            skipped: summary.skipped,
            failed: summary.failed,
            totalAmount: summary.totalAmount
        });
        return summary;
    }
    async resolveVault(vaultId) {
        if (vaultId) {
            try {
                const vault = await SweepVaultModel_js_1.SweepVaultModel.findById(vaultId).lean();
                if (vault)
                    return vault;
            }
            catch {
                // Invalid vault id format; fall through to active vault lookup.
            }
        }
        return SweepVaultModel_js_1.SweepVaultModel.findOne({ isActive: true }).lean();
    }
    groupByWallet(transactions) {
        const map = new Map();
        for (const tx of transactions) {
            const key = `${tx.network}:${tx.walletAddress.toLowerCase()}`;
            const existing = map.get(key);
            if (existing) {
                existing.transactionIds.push(tx._id.toString());
            }
            else {
                map.set(key, {
                    network: tx.network,
                    walletAddress: tx.walletAddress,
                    transactionIds: [tx._id.toString()]
                });
            }
        }
        return Array.from(map.values());
    }
    async sweepWallet(network, walletAddress, transactionIds, vault) {
        try {
            const wallet = await PaymentWalletModel_js_1.PaymentWalletModel.findOne({ network, walletAddress });
            if (!wallet) {
                await this.recordFailure(transactionIds, "Wallet record not found.");
                return { network, walletAddress, amount: "0", error: "Wallet record not found." };
            }
            if (!wallet.encryptedPrivateKey) {
                await this.recordFailure(transactionIds, "Wallet has no stored private key.");
                return { network, walletAddress, amount: "0", error: "Wallet has no stored private key." };
            }
            const vaultAddress = this.resolveVaultAddress(network, vault);
            if (!vaultAddress) {
                await this.recordFailure(transactionIds, `No vault address configured for ${network}.`);
                return { network, walletAddress, amount: "0", error: `No vault address configured for ${network}.` };
            }
            const privateKey = this.cryptoBox.decrypt(wallet.encryptedPrivateKey);
            const result = network === "tron"
                ? await this.sweepTronWallet(privateKey, walletAddress, vaultAddress, transactionIds, vault?._id.toString())
                : await this.sweepEvmWallet(network, privateKey, walletAddress, vaultAddress, transactionIds, vault?._id.toString());
            return { network, walletAddress, ...result };
        }
        catch (error) {
            const message = error instanceof Error ? error.message : String(error);
            await this.recordFailure(transactionIds, message);
            return { network, walletAddress, amount: "0", error: message };
        }
    }
    resolveVaultAddress(network, vault) {
        if (network === "tron") {
            return vault?.tronAddress ?? this.config.tronVaultAddress;
        }
        return vault?.evmAddress ?? this.config.evmVaultAddress;
    }
    async sweepEvmWallet(network, privateKey, walletAddress, vaultAddress, transactionIds, vaultId) {
        const networkConfig = this.config.paymentNetworks[network];
        if (!networkConfig) {
            await this.recordFailure(transactionIds, `${network} payments are not configured.`);
            return { amount: "0", error: `${network} payments are not configured.` };
        }
        if (!ethers_1.ethers.isAddress(vaultAddress)) {
            await this.recordFailure(transactionIds, "Vault address is invalid.");
            return { amount: "0", error: "Vault address is invalid." };
        }
        const provider = new ethers_1.ethers.JsonRpcProvider(networkConfig.rpcUrl);
        const signer = new ethers_1.ethers.Wallet(privateKey, provider);
        if (signer.address.toLowerCase() !== walletAddress.toLowerCase()) {
            await this.recordFailure(transactionIds, "Private key does not match wallet address.");
            return { amount: "0", error: "Private key does not match wallet address." };
        }
        const contract = new ethers_1.ethers.Contract(networkConfig.usdtAddress, erc20Abi, signer);
        const balance = (await contract.balanceOf(walletAddress));
        if (balance <= 0n) {
            await this.markSwept(transactionIds, { sweptAmount: "0" });
            return { amount: "0" };
        }
        const gasLimit = await contract.transfer.estimateGas(vaultAddress, balance);
        const feeData = await provider.getFeeData();
        const gasPrice = feeData.maxFeePerGas ?? feeData.gasPrice ?? 0n;
        if (gasPrice === 0n) {
            await this.recordFailure(transactionIds, "Could not determine gas price.");
            return { amount: "0", error: "Could not determine gas price." };
        }
        const sweepGasEstimate = gasLimit * gasPrice;
        const nativeBalance = await provider.getBalance(walletAddress);
        let gasPreparation;
        if (nativeBalance < sweepGasEstimate) {
            gasPreparation = await this.gasService.ensureVaultGasForSweep(network, walletAddress, sweepGasEstimate, vaultId);
            if (!gasPreparation.funded) {
                const reason = gasPreparation.error ?? "Vault could not fund gas for sweep.";
                await this.recordFailure(transactionIds, reason);
                return { amount: "0", error: reason };
            }
            // Poll briefly until the payment wallet balance reflects the top-up.
            const funded = await this.pollForNativeBalance(provider, walletAddress, sweepGasEstimate, 10, 3000);
            if (!funded) {
                const reason = "Gas top-up was sent but payment wallet balance did not update in time.";
                await this.recordFailure(transactionIds, reason);
                return {
                    amount: "0",
                    error: reason,
                    feeTxHash: gasPreparation.feeTxHash,
                    swapTxHash: gasPreparation.swapTxHash
                };
            }
        }
        const tx = await contract.transfer(vaultAddress, balance);
        const receipt = await tx.wait();
        if (!receipt) {
            const reason = "Transaction broadcast but receipt was not returned.";
            await this.recordFailure(transactionIds, reason);
            return { amount: "0", error: reason };
        }
        await this.markSwept(transactionIds, { sweptAmount: balance.toString(), sweepTxHash: tx.hash });
        return {
            amount: balance.toString(),
            txHash: tx.hash,
            feeTxHash: gasPreparation?.feeTxHash,
            swapTxHash: gasPreparation?.swapTxHash
        };
    }
    async sweepTronWallet(privateKey, walletAddress, vaultAddress, transactionIds, vaultId) {
        const networkConfig = this.config.paymentNetworks.tron;
        if (!networkConfig) {
            await this.recordFailure(transactionIds, "Tron payments are not configured.");
            return { amount: "0", error: "Tron payments are not configured." };
        }
        if (!tronweb_1.TronWeb.isAddress(vaultAddress)) {
            await this.recordFailure(transactionIds, "Tron vault address is invalid.");
            return { amount: "0", error: "Tron vault address is invalid." };
        }
        const tronPrivateKey = privateKey.startsWith("0x") ? privateKey.slice(2) : privateKey;
        const tronWeb = new tronweb_1.TronWeb({
            fullHost: networkConfig.fullHost,
            headers: this.tronHeaders(networkConfig.apiKey),
            privateKey: tronPrivateKey
        });
        const derivedAddress = tronWeb.defaultAddress.base58;
        if (!derivedAddress || derivedAddress.toLowerCase() !== walletAddress.toLowerCase()) {
            await this.recordFailure(transactionIds, "Private key does not match Tron wallet address.");
            return { amount: "0", error: "Private key does not match Tron wallet address." };
        }
        const contract = await tronWeb.contract(trc20Abi, networkConfig.usdtContract);
        const balanceResponse = await contract.balanceOf(walletAddress).call();
        const balance = BigInt(balanceResponse.toString());
        if (balance <= 0n) {
            await this.markSwept(transactionIds, { sweptAmount: "0" });
            return { amount: "0" };
        }
        // Tron energy/bandwidth is hard to estimate precisely. Use a fixed TRX buffer.
        const sweepGasEstimate = 30n * 1000000n; // 30 TRX
        const nativeBalance = BigInt(await tronWeb.trx.getBalance(walletAddress));
        let gasPreparation;
        if (nativeBalance < sweepGasEstimate) {
            gasPreparation = await this.gasService.ensureVaultGasForSweep("tron", walletAddress, sweepGasEstimate, vaultId);
            if (!gasPreparation.funded) {
                const reason = gasPreparation.error ?? "Vault could not fund TRX for sweep.";
                await this.recordFailure(transactionIds, reason);
                return { amount: "0", error: reason };
            }
            const funded = await this.pollForTronBalance(tronWeb, walletAddress, sweepGasEstimate, 10, 3000);
            if (!funded) {
                const reason = "TRX top-up was sent but payment wallet balance did not update in time.";
                await this.recordFailure(transactionIds, reason);
                return {
                    amount: "0",
                    error: reason,
                    feeTxHash: gasPreparation.feeTxHash,
                    swapTxHash: gasPreparation.swapTxHash
                };
            }
        }
        const txResult = await contract.transfer(vaultAddress, balance.toString()).send();
        const txHash = typeof txResult === "string" ? txResult : txResult?.transaction?.txID ?? txResult?.txID;
        if (!txHash) {
            const reason = "Tron transfer did not return a transaction hash.";
            await this.recordFailure(transactionIds, reason);
            return { amount: "0", error: reason };
        }
        await this.markSwept(transactionIds, { sweptAmount: balance.toString(), sweepTxHash: txHash });
        return {
            amount: balance.toString(),
            txHash,
            feeTxHash: gasPreparation?.feeTxHash,
            swapTxHash: gasPreparation?.swapTxHash
        };
    }
    async pollForNativeBalance(provider, address, minimum, maxAttempts, intervalMs) {
        for (let i = 0; i < maxAttempts; i++) {
            const balance = await provider.getBalance(address);
            if (balance >= minimum) {
                return true;
            }
            await new Promise((resolve) => setTimeout(resolve, intervalMs));
        }
        return false;
    }
    async pollForTronBalance(tronWeb, address, minimum, maxAttempts, intervalMs) {
        for (let i = 0; i < maxAttempts; i++) {
            const balance = BigInt(await tronWeb.trx.getBalance(address));
            if (balance >= minimum) {
                return true;
            }
            await new Promise((resolve) => setTimeout(resolve, intervalMs));
        }
        return false;
    }
    async markSwept(transactionIds, fields) {
        await PaymentTransactionModel_js_1.PaymentTransactionModel.updateMany({ _id: { $in: transactionIds.map((id) => new mongoose_1.default.Types.ObjectId(id)) } }, {
            $set: {
                sweptAt: new Date(),
                sweptAmount: fields.sweptAmount,
                ...(fields.sweepTxHash ? { sweepTxHash: fields.sweepTxHash } : {})
            }
        });
    }
    async recordFailure(transactionIds, reason) {
        await PaymentTransactionModel_js_1.PaymentTransactionModel.updateMany({ _id: { $in: transactionIds.map((id) => new mongoose_1.default.Types.ObjectId(id)) } }, { $set: { sweepFailureReason: reason } });
    }
    tronHeaders(apiKey) {
        return apiKey ? { "TRON-PRO-API-KEY": apiKey } : undefined;
    }
}
exports.PaymentSweeper = PaymentSweeper;
