// Decode Uniswap Universal Router execute() calldata into human-readable
// swap details.  Designed to be extended with other DEX decoders later.

const { Interface, AbiCoder, getBytes, formatUnits } = require("ethers");
const { TOKEN_BY_ADDRESS } = require("./tokenList");

const coder = AbiCoder.defaultAbiCoder();

const ROUTER_IFACE = new Interface([
    "function execute(bytes commands, bytes[] inputs, uint256 deadline)",
]);

// Universal Router command IDs (lower 5 bits of each command byte)
const COMMAND_NAMES = {
    0x00: "V3 Swap (Exact In)",
    0x01: "V3 Swap (Exact Out)",
    0x02: "Permit2 Transfer",
    0x03: "Permit2 Permit Batch",
    0x04: "Sweep",
    0x05: "Transfer",
    0x06: "Pay Portion",
    0x08: "V2 Swap (Exact In)",
    0x09: "V2 Swap (Exact Out)",
    0x0a: "Permit2 Permit",
    0x0b: "Wrap ETH",
    0x0c: "Unwrap WETH",
    0x0d: "Permit2 Transfer Batch",
    0x0e: "Balance Check",
    0x10: "V4 Swap",
    0x11: "V3 Position Mgr Permit",
    0x12: "V3 Position Mgr Call",
    0x13: "V4 Initialize Pool",
    0x14: "V4 Position Mgr Call",
    0x21: "Execute Sub-Plan",
};

function formatAmount(raw, decimals) {
    const parts = formatUnits(raw, decimals).split(".");
    if (parts.length === 1) return parts[0] + ".0000";
    const dec = (parts[1] + "0000").slice(0, 4);
    return parts[0] + "." + dec;
}

function tokenInfo(address) {
    if (!address || address === "0x0000000000000000000000000000000000000000") {
        return { symbol: "ETH", decimals: 18, address: null };
    }
    const t = TOKEN_BY_ADDRESS.get(address.toLowerCase());
    if (t) return { symbol: t.symbol, decimals: t.decimals, address };
    return { symbol: null, decimals: 18, address };
}

// Decode PERMIT2_PERMIT (command 0x0a) input bytes.
// ABI: ((address token, uint160 amount, uint48 expiration, uint48 nonce),
//        address spender, uint256 sigDeadline), bytes signature
function decodePermit2(input) {
    try {
        const d = coder.decode(
            [
                "tuple(tuple(address,uint160,uint48,uint48),address,uint256)",
                "bytes",
            ],
            input,
        );
        return { token: d[0][0][0], amount: d[0][0][1], spender: d[0][1] };
    } catch {
        return null;
    }
}

// Decode BALANCE_CHECK_ERC20 (command 0x0e) input bytes.
// ABI: (address owner, address token, uint256 minBalance)
function decodeBalanceCheck(input) {
    try {
        const d = coder.decode(["address", "address", "uint256"], input);
        return { owner: d[0], token: d[1], minBalance: d[2] };
    } catch {
        return null;
    }
}

// Decode V2_SWAP_EXACT_IN (command 0x08) input bytes.
// ABI: (address recipient, uint256 amountIn, uint256 amountOutMin,
//        address[] path, bool payerIsUser)
function decodeV2SwapExactIn(input) {
    try {
        const d = coder.decode(
            ["address", "uint256", "uint256", "address[]", "bool"],
            input,
        );
        return {
            amountIn: d[1],
            amountOutMin: d[2],
            tokenIn: d[3][0],
            tokenOut: d[3][d[3].length - 1],
        };
    } catch {
        return null;
    }
}

// Decode V2_SWAP_EXACT_OUT (command 0x09) input bytes.
// ABI: (address recipient, uint256 amountOut, uint256 amountInMax,
//        address[] path, bool payerIsUser)
function decodeV2SwapExactOut(input) {
    try {
        const d = coder.decode(
            ["address", "uint256", "uint256", "address[]", "bool"],
            input,
        );
        return {
            amountOut: d[1],
            amountInMax: d[2],
            tokenIn: d[3][0],
            tokenOut: d[3][d[3].length - 1],
        };
    } catch {
        return null;
    }
}

// Decode V3 swap path (packed: token(20) + fee(3) + token(20) ...)
function decodeV3Path(pathHex) {
    const hex = pathHex.startsWith("0x") ? pathHex.slice(2) : pathHex;
    if (hex.length < 40) return null;
    const tokenIn = "0x" + hex.slice(0, 40);
    const tokenOut = "0x" + hex.slice(-40);
    return { tokenIn, tokenOut };
}

// Decode V3_SWAP_EXACT_IN (command 0x00) input bytes.
// ABI: (address recipient, uint256 amountIn, uint256 amountOutMin,
//        bytes path, bool payerIsUser)
function decodeV3SwapExactIn(input) {
    try {
        const d = coder.decode(
            ["address", "uint256", "uint256", "bytes", "bool"],
            input,
        );
        const path = decodeV3Path(d[3]);
        if (!path) return null;
        return {
            amountIn: d[1],
            amountOutMin: d[2],
            tokenIn: path.tokenIn,
            tokenOut: path.tokenOut,
        };
    } catch {
        return null;
    }
}

// Decode WRAP_ETH (command 0x0b) input bytes.
// ABI: (address recipient, uint256 amount)
function decodeWrapEth(input) {
    try {
        const d = coder.decode(["address", "uint256"], input);
        return { amount: d[1] };
    } catch {
        return null;
    }
}

// V4 inner action IDs
const V4_SWAP_EXACT_IN_SINGLE = 0x06;
const V4_SWAP_EXACT_IN = 0x07;
const V4_SWAP_EXACT_OUT_SINGLE = 0x08;
const V4_SWAP_EXACT_OUT = 0x09;
const V4_SETTLE = 0x0b;
const V4_TAKE = 0x0e;

// Decode V4_SWAP (command 0x10) input bytes.
// The input is ABI-encoded as (bytes actions, bytes[] params).
// We extract token addresses from SETTLE (input) and TAKE (output) sub-actions,
// and swap amounts from the swap sub-actions.
function decodeV4Swap(input) {
    try {
        const d = coder.decode(["bytes", "bytes[]"], input);
        const actions = getBytes(d[0]);
        const params = d[1];

        let settleToken = null;
        let takeToken = null;
        let amountIn = null;
        let amountOutMin = null;

        for (let i = 0; i < actions.length; i++) {
            const actionId = actions[i];
            try {
                if (actionId === V4_SETTLE) {
                    // SETTLE: (address currency, uint256 maxAmount, bool payerIsUser)
                    const s = coder.decode(
                        ["address", "uint256", "bool"],
                        params[i],
                    );
                    settleToken = s[0];
                } else if (actionId === V4_TAKE) {
                    // TAKE: (address currency, address recipient, uint256 amount)
                    const t = coder.decode(
                        ["address", "address", "uint256"],
                        params[i],
                    );
                    takeToken = t[0];
                } else if (
                    actionId === V4_SWAP_EXACT_IN ||
                    actionId === V4_SWAP_EXACT_IN_SINGLE
                ) {
                    // Extract amounts from exact-in swap actions
                    if (actionId === V4_SWAP_EXACT_IN) {
                        // ExactInputParams: (address currencyIn,
                        //   tuple(address,uint24,int24,address,bytes)[] path,
                        //   uint128 amountIn, uint128 amountOutMin)
                        try {
                            const s = coder.decode(
                                [
                                    "tuple(address,tuple(address,uint24,int24,address,bytes)[],uint128,uint128)",
                                ],
                                params[i],
                            );
                            if (!settleToken) settleToken = s[0][0];
                            const path = s[0][1];
                            if (path.length > 0 && !takeToken) {
                                takeToken = path[path.length - 1][0];
                            }
                            if (!amountIn) amountIn = s[0][2];
                            if (!amountOutMin) amountOutMin = s[0][3];
                        } catch {
                            // Fall through — SETTLE/TAKE will provide tokens
                        }
                    } else {
                        // ExactInputSingleParams: (tuple(address,address,uint24,int24,address) poolKey,
                        //   bool zeroForOne, uint128 amountIn, uint128 amountOutMin, bytes hookData)
                        try {
                            const s = coder.decode(
                                [
                                    "tuple(tuple(address,address,uint24,int24,address),bool,uint128,uint128,bytes)",
                                ],
                                params[i],
                            );
                            const poolKey = s[0][0];
                            const zeroForOne = s[0][1];
                            if (!settleToken)
                                settleToken = zeroForOne
                                    ? poolKey[0]
                                    : poolKey[1];
                            if (!takeToken)
                                takeToken = zeroForOne
                                    ? poolKey[1]
                                    : poolKey[0];
                            if (!amountIn) amountIn = s[0][2];
                            if (!amountOutMin) amountOutMin = s[0][3];
                        } catch {
                            // Fall through
                        }
                    }
                } else if (
                    actionId === V4_SWAP_EXACT_OUT ||
                    actionId === V4_SWAP_EXACT_OUT_SINGLE
                ) {
                    if (actionId === V4_SWAP_EXACT_OUT) {
                        try {
                            const s = coder.decode(
                                [
                                    "tuple(address,tuple(address,uint24,int24,address,bytes)[],uint128,uint128)",
                                ],
                                params[i],
                            );
                            if (!takeToken) takeToken = s[0][0];
                            const path = s[0][1];
                            if (path.length > 0 && !settleToken) {
                                settleToken = path[path.length - 1][0];
                            }
                        } catch {
                            // Fall through
                        }
                    } else {
                        try {
                            const s = coder.decode(
                                [
                                    "tuple(tuple(address,address,uint24,int24,address),bool,uint128,uint128,bytes)",
                                ],
                                params[i],
                            );
                            const poolKey = s[0][0];
                            const zeroForOne = s[0][1];
                            if (!settleToken)
                                settleToken = zeroForOne
                                    ? poolKey[0]
                                    : poolKey[1];
                            if (!takeToken)
                                takeToken = zeroForOne
                                    ? poolKey[1]
                                    : poolKey[0];
                        } catch {
                            // Fall through
                        }
                    }
                }
            } catch {
                // Skip sub-actions we can't decode
            }
        }

        return {
            tokenIn: settleToken,
            tokenOut: takeToken,
            amountIn,
            amountOutMin,
        };
    } catch {
        return null;
    }
}

// Try to decode a Universal Router execute() call.
// Returns { name, description, details } matching the format used by
// the approval UI, or null if the calldata is not a recognised execute().
function decode(data, toAddress) {
    try {
        const parsed = ROUTER_IFACE.parseTransaction({ data });
        if (!parsed) return null;

        const commandsBytes = getBytes(parsed.args[0]);
        const inputs = parsed.args[1];
        const deadline = parsed.args[2];

        let inputToken = null;
        let inputAmount = null;
        let outputToken = null;
        let minOutput = null;
        let hasUnwrapWeth = false;
        const commandNames = [];

        for (let i = 0; i < commandsBytes.length; i++) {
            const cmdId = commandsBytes[i] & 0x1f;
            commandNames.push(
                COMMAND_NAMES[cmdId] ||
                    "Command 0x" + cmdId.toString(16).padStart(2, "0"),
            );

            try {
                if (cmdId === 0x0a) {
                    const p = decodePermit2(inputs[i]);
                    if (p) {
                        inputToken = p.token;
                        inputAmount = p.amount;
                    }
                }

                if (cmdId === 0x0e) {
                    const b = decodeBalanceCheck(inputs[i]);
                    if (b) {
                        outputToken = b.token;
                        minOutput = b.minBalance;
                    }
                }

                if (cmdId === 0x00) {
                    const s = decodeV3SwapExactIn(inputs[i]);
                    if (s) {
                        if (!inputToken) inputToken = s.tokenIn;
                        if (!inputAmount) inputAmount = s.amountIn;
                        // Always update output: in multi-step swaps (V3 → V4),
                        // the last swap step determines the final output token
                        // and minimum received amount.
                        outputToken = s.tokenOut;
                        minOutput = s.amountOutMin;
                    }
                }

                if (cmdId === 0x08) {
                    const s = decodeV2SwapExactIn(inputs[i]);
                    if (s) {
                        if (!inputToken) inputToken = s.tokenIn;
                        if (!inputAmount) inputAmount = s.amountIn;
                        outputToken = s.tokenOut;
                        minOutput = s.amountOutMin;
                    }
                }

                if (cmdId === 0x0b) {
                    const w = decodeWrapEth(inputs[i]);
                    if (w && !inputToken) {
                        inputToken =
                            "0x0000000000000000000000000000000000000000";
                        inputAmount = w.amount;
                    }
                }

                if (cmdId === 0x10) {
                    const v4 = decodeV4Swap(inputs[i]);
                    if (v4) {
                        if (!inputToken && v4.tokenIn) inputToken = v4.tokenIn;
                        if (!inputAmount && v4.amountIn)
                            inputAmount = v4.amountIn;
                        // Always update output: last swap step wins
                        if (v4.tokenOut) outputToken = v4.tokenOut;
                        if (v4.amountOutMin) minOutput = v4.amountOutMin;
                    }
                }

                if (cmdId === 0x0c) {
                    hasUnwrapWeth = true;
                }
            } catch {
                // Skip commands we can't decode
            }
        }

        // Resolve token info
        const inInfo = tokenInfo(inputToken);
        const outInfo = hasUnwrapWeth
            ? { symbol: "ETH", decimals: 18, address: null }
            : tokenInfo(outputToken);

        const inSymbol = inInfo.symbol;
        const outSymbol = outInfo.symbol;

        const name =
            inSymbol && outSymbol
                ? "Swap " + inSymbol + " \u2192 " + outSymbol
                : "Uniswap Swap";

        const details = [];

        details.push({
            label: "Protocol",
            value: "Uniswap Universal Router",
            address: toAddress,
        });

        if (inputToken && inInfo.address) {
            const label = inSymbol
                ? inSymbol + " (" + inputToken + ")"
                : inputToken;
            details.push({
                label: "Token In",
                value: label,
                address: inputToken,
                isToken: true,
            });
        } else if (inSymbol === "ETH") {
            details.push({ label: "Token In", value: "ETH (native)" });
        }

        if (inputAmount !== null && inputAmount !== undefined) {
            const maxUint160 = BigInt(
                "0xffffffffffffffffffffffffffffffffffffffff",
            );
            const isUnlimited = inputAmount >= maxUint160;
            const amountRaw = isUnlimited
                ? "Unlimited"
                : formatAmount(inputAmount, inInfo.decimals);
            const amountStr = isUnlimited
                ? "Unlimited"
                : amountRaw + (inSymbol ? " " + inSymbol : "");
            details.push({
                label: "Amount",
                value: amountStr,
                rawValue: amountRaw,
            });
        }

        if (outSymbol) {
            if (outInfo.address) {
                const label = outSymbol
                    ? outSymbol + " (" + outputToken + ")"
                    : outputToken;
                details.push({
                    label: "Token Out",
                    value: label,
                    address: outputToken,
                    isToken: true,
                });
            } else {
                details.push({ label: "Token Out", value: outSymbol });
            }
        }

        if (minOutput !== null && minOutput !== undefined) {
            const minStr =
                formatAmount(minOutput, outInfo.decimals) +
                (outSymbol ? " " + outSymbol : "");
            details.push({ label: "Min. received", value: minStr });
        }

        details.push({ label: "Steps", value: commandNames.join(" \u2192 ") });

        const deadlineDate = new Date(Number(deadline) * 1000);
        details.push({
            label: "Deadline",
            value: deadlineDate.toISOString().replace("T", " ").slice(0, 19),
        });

        return {
            name,
            description: "Swap via Uniswap Universal Router",
            details,
        };
    } catch {
        return null;
    }
}

module.exports = { decode };
