// SPDX-License-Identifier: MIT
pragma solidity 0.8.23;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";

/**
 * @title AereAccountMigrator, atomic asset migration from a classical EOA to a
 *        post-quantum account (roadmap #14, quantum layer 11)
 *
 * @notice The last quantum-resistance gap at the ACCOUNT layer. Aere Network
 *         already ships live post-quantum smart accounts (AerePQCAccount,
 *         Falcon-512 owned), a live CREATE2 factory (AerePQCAccountFactory), and a
 *         hybrid ECDSA + Falcon primitive (AereHybridAuth). What was missing was a
 *         clean, no-custody path for a holder who is still sitting on a classical
 *         secp256k1 ECDSA externally-owned account (EOA) to MOVE their assets onto
 *         one of those post-quantum accounts in a single, self-authorized
 *         transaction. This contract is that path.
 *
 *         The motivation is harvest-now-decrypt-later. An ECDSA key is
 *         Shor-breakable by a future quantum computer, and an EOA's public key is
 *         exposed the moment it sends its first transaction. Long-term holdings
 *         therefore should not sit under a classical key indefinitely; they should
 *         move to a Falcon-512-owned (AerePQCAccount) or hybrid (AereHybridAuth)
 *         account whose spending authority is a lattice key.
 *
 * @dev    NO CUSTODY, NO ADMIN. This contract is a pure conduit. In `migrate` the
 *         caller (the old EOA) is the `from` of every SafeERC20.safeTransferFrom,
 *         and the caller-specified `destination` is the `to`. Tokens move
 *         caller -> destination in one hop; the migrator never holds an ERC-20
 *         balance and never becomes the recipient. Native AERE (msg.value) is
 *         forwarded, in full, to that same `destination` inside the same call.
 *         There is:
 *           - no owner, no admin, no upgrade, no initializer;
 *           - no withdraw / rescue / sweep function, so nothing accidentally sent
 *             to this address can ever be pulled out by anyone;
 *           - no code path that sends value to any address other than the
 *             caller-specified `destination`.
 *
 * @dev    FAIL-CLOSED / ATOMIC. `destination` must be non-zero. Token and amount
 *         arrays must be equal length. Every entry must be a non-zero token and a
 *         non-zero amount. Native handling is strict: `moveNative` true requires a
 *         non-zero msg.value, and any msg.value sent while `moveNative` is false
 *         reverts (StrayNative) so value can never be stranded in the migrator. A
 *         single failed transfer reverts the WHOLE transaction (SafeERC20 reverts
 *         on a false/failing ERC-20), so a migration is all-or-nothing: it can
 *         never leave a holder with a partially drained old account.
 *
 * @dev    AUTHORIZATION. The move is authorized by the caller's classical ECDSA
 *         key. Two supply patterns are supported and documented:
 *
 *         1. PRE-APPROVAL (works today, no protocol feature). The holder sends one
 *            `approve(migrator, amount)` per ERC-20 to this contract, then calls
 *            `migrate`. Each `safeTransferFrom` consumes the holder's allowance and
 *            moves the tokens straight to the destination. This is the default,
 *            portable path.
 *
 *         2. EIP-7702 DELEGATION (single-transaction UX). Aere supports EIP-7702
 *            (see docs/AERE-EIP-COMPATIBILITY-MATRIX.md, 7702 Supported), so the
 *            EOA can, in one transaction, temporarily set its account code to a
 *            batching delegate that both approves and calls `migrate`, removing the
 *            separate approve transactions. The migrator itself is agnostic to
 *            which pattern is used; under EIP-7702 the caller is still the EOA and
 *            the no-custody invariants above are unchanged.
 *
 *         The destination is chosen by the caller. `migrateToPqcAccount` binds the
 *         destination to a real Falcon-512 key by deriving it from the LIVE
 *         AerePQCAccountFactory (predictAddress is pure CREATE2 math, so it works
 *         even before the account is deployed), giving the holder cryptographic
 *         assurance that they are sweeping into the post-quantum account that their
 *         Falcon key controls, and nowhere else.
 *
 * @dev    SCOPE BOUNDARY (unchanged, non-negotiable). Migrating an account's
 *         authorization to a post-quantum scheme does NOT make Aere consensus
 *         post-quantum. Mainnet 2800 still seals blocks with classical secp256k1
 *         QBFT, and the on-chain ZK verifiers (BN254 Groth16) are classical. This
 *         contract changes who can spend an account's assets; it does not touch the
 *         consensus layer. Network-key rotation (Foundation operator key, validator
 *         signing keys) is a separate, founder-and-audit-gated operation, coupled
 *         to the consensus-PQC activation, and is documented as design-only in
 *         docs/AERE-QUANTUM-MIGRATION.md.
 */
interface IAerePQCAccountFactory {
    /// @notice The counterfactual AerePQCAccount address for (falconPubKey, salt).
    ///         Pure CREATE2 math; returns an address whether or not it is deployed.
    function predictAddress(bytes calldata falconPubKey, uint256 salt) external view returns (address);
}

contract AereAccountMigrator is ReentrancyGuard {
    using SafeERC20 for IERC20;

    /// @notice Emitted once per successful migration, binding the old (classical)
    ///         account, the new (post-quantum) destination account, and the exact
    ///         asset set that moved.
    event Migrated(
        address indexed oldAccount,
        address indexed newAccount,
        address[] tokens,
        uint256[] amounts,
        uint256 nativeAmount
    );

    error ZeroDestination();
    error LengthMismatch();
    error NothingToMigrate();
    error ZeroToken();
    error ZeroAmount();
    error ZeroNative();
    error StrayNative();
    error NativeForwardFailed();
    error AddressMismatch();

    /**
     * @notice Atomically move a set of ERC-20 balances (and optionally native
     *         AERE) from the caller (the old classical EOA) to `destination` (a
     *         post-quantum account address). Authorized by the caller's ECDSA key;
     *         requires a prior `approve(this, amount)` per ERC-20, or an EIP-7702
     *         delegated batch that performs the approvals in the same transaction.
     *
     * @param destination the post-quantum account to receive everything. Must be
     *                     non-zero. This is the ONLY recipient of any asset moved.
     * @param tokens       the ERC-20 tokens to move (each non-zero).
     * @param amounts      the amount of each token to move (each non-zero, same
     *                     length as `tokens`).
     * @param moveNative   if true, forward the full msg.value to `destination`; if
     *                     false, msg.value must be zero.
     */
    function migrate(
        address destination,
        address[] calldata tokens,
        uint256[] calldata amounts,
        bool moveNative
    ) external payable nonReentrant {
        _migrate(destination, tokens, amounts, moveNative);
    }

    /**
     * @notice Same as `migrate`, but the destination is DERIVED from a Falcon-512
     *         public key via the live AerePQCAccountFactory, so the holder cannot
     *         accidentally (or be tricked into) sweeping into the wrong address:
     *         the destination is exactly the post-quantum account controlled by
     *         their Falcon key. Works even before that account is deployed, because
     *         `predictAddress` is pure CREATE2 math (the account can hold assets at
     *         its counterfactual address and be deployed later via the factory).
     *
     * @param factory             the AerePQCAccountFactory (the live one on 2800).
     * @param falconPubKey        the 897-byte NIST Falcon-512 public key that will
     *                            own the destination account.
     * @param salt                the CREATE2 salt used with the factory.
     * @param expectedDestination optional guard: if non-zero, the derived address
     *                            must equal it, else revert AddressMismatch. Lets a
     *                            caller assert the address they reviewed off-chain.
     * @return destination        the derived post-quantum account address.
     */
    function migrateToPqcAccount(
        address factory,
        bytes calldata falconPubKey,
        uint256 salt,
        address expectedDestination,
        address[] calldata tokens,
        uint256[] calldata amounts,
        bool moveNative
    ) external payable nonReentrant returns (address destination) {
        destination = IAerePQCAccountFactory(factory).predictAddress(falconPubKey, salt);
        if (expectedDestination != address(0) && expectedDestination != destination) {
            revert AddressMismatch();
        }
        _migrate(destination, tokens, amounts, moveNative);
    }

    /**
     * @notice Read-only helper: the counterfactual AerePQCAccount address for a
     *         Falcon key, delegating to the factory. Lets a frontend confirm the
     *         destination before signing, without trusting an off-chain derivation.
     */
    function predictPqcAccount(address factory, bytes calldata falconPubKey, uint256 salt)
        external
        view
        returns (address)
    {
        return IAerePQCAccountFactory(factory).predictAddress(falconPubKey, salt);
    }

    /* -------------------------------- internal ------------------------------- */

    function _migrate(
        address destination,
        address[] calldata tokens,
        uint256[] calldata amounts,
        bool moveNative
    ) internal {
        if (destination == address(0)) revert ZeroDestination();
        if (tokens.length != amounts.length) revert LengthMismatch();

        uint256 nativeAmount = msg.value;
        if (moveNative) {
            if (nativeAmount == 0) revert ZeroNative();
        } else {
            // Fail-closed: never accept native the caller did not ask to move; it
            // would otherwise be stranded (there is no withdraw function).
            if (nativeAmount != 0) revert StrayNative();
        }
        if (tokens.length == 0 && !moveNative) revert NothingToMigrate();

        // Move every ERC-20 straight from the caller to the destination. The
        // migrator is never the `to`, so it never takes custody. Any failing
        // transfer reverts the whole call (SafeERC20), so the migration is atomic.
        for (uint256 i = 0; i < tokens.length; i++) {
            address token = tokens[i];
            uint256 amount = amounts[i];
            if (token == address(0)) revert ZeroToken();
            if (amount == 0) revert ZeroAmount();
            IERC20(token).safeTransferFrom(msg.sender, destination, amount);
        }

        // Forward native AERE, in full, to the SAME destination. Last, after the
        // token moves, and guarded by nonReentrant so a hostile destination cannot
        // reenter mid-migration.
        if (moveNative) {
            (bool ok, ) = payable(destination).call{value: nativeAmount}("");
            if (!ok) revert NativeForwardFailed();
        }

        emit Migrated(msg.sender, destination, tokens, amounts, moveNative ? nativeAmount : 0);
    }
}
