import { Logger } from './logger.js';
import path from 'path';
import { fileURLToPath } from 'url';
import dotenv from 'dotenv';
import { wallet, block, tools } from "nanocurrency-web";

// Load environment variables
dotenv.config();

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

// API settings
const WORK_API_KEY = "RPC-KEY-BAB822FCCDAE42ECB7A331CCAAAA23";
const RPC_API_KEY = "PUBLIC-KEY-FA9CE81226BF478291D34836A09D8B06";
const MIN_DELAY_BETWEEN_REQUESTS = 1000; // 1 second minimum delay
let lastRequestTime = 0;

async function sleep(ms: number) {
    return new Promise(resolve => setTimeout(resolve, ms));
}

async function rpc(request: any, useWorkKey = false) {
    // Implement rate limiting
    const now = Date.now();
    const timeSinceLastRequest = now - lastRequestTime;
    if (timeSinceLastRequest < MIN_DELAY_BETWEEN_REQUESTS) {
        await sleep(MIN_DELAY_BETWEEN_REQUESTS - timeSinceLastRequest);
    }
    lastRequestTime = Date.now();

    // Determine which API key to use based on the action
    const shouldUseWorkKey = request.action === 'work_generate';
    
    // Add API key to request
    const requestWithKey = {
        ...request,
        key: shouldUseWorkKey ? WORK_API_KEY : RPC_API_KEY
    };

    console.log('Request:', {
        ...requestWithKey,
        key: shouldUseWorkKey ? 'WORK_API_KEY' : 'RPC_API_KEY' // Hide actual key in logs
    });

    const response = await fetch(`https://rpc.nano.to`, {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
            'Accept': 'application/json'
        },
        body: JSON.stringify(requestWithKey)
    });

    let data;

    try {
        data = await response.json();
    } catch {
        if (response.status === 429) {
            console.log("Rate limited, waiting 5 seconds...");
            await sleep(5000);
            return rpc(request, useWorkKey); // Retry the request
        }
        throw Error(`RPC status ${response.status}: failed to parse JSON`);
    }

    // Handle rate limiting responses
    if (response.status === 429) {
        console.log("Rate limited by status code, waiting 5 seconds...");
        await sleep(5000);
        return rpc(request, useWorkKey); // Retry the request
    }

    // Handle error responses
    if ('error' in data) {
        const errorMessage = typeof data.error === 'string' ? data.error : JSON.stringify(data.error);
        
        // Check for rate limiting errors
        if (errorMessage.toLowerCase().includes('rate') || errorMessage.toLowerCase().includes('too many requests')) {
            console.log("Rate limited by error response, waiting 5 seconds...");
            await sleep(5000);
            return rpc(request, useWorkKey); // Retry the request
        }

        console.error(`RPC error: ${errorMessage}`);
        throw Error(errorMessage);
    }

    if (!response.ok) {
        throw Error(`RPC status ${response.status}: ${JSON.stringify(data, null, 4)}`);
    }

    console.log(data);
    return data;
}

async function generateWork(hash: string, isOpen = false): Promise<string> {
    try {
        const data = await rpc({
            action: "work_generate",
            hash: hash,
            difficulty: isOpen ? 'fffffff800000000' : 'fffffff800000000'
        }); // No need to pass useWorkKey flag anymore since we check action type

        if (!data || !data.work) {
            throw Error("No work returned from RPC");
        }

        return data.work;
    } catch (error: any) {
        throw Error(`Failed to generate work: ${error?.message || 'Unknown error'}`);
    }
}

async function balance(account: string) {
    let retries = 3;
    while (retries > 0) {
        try {
            const data = await rpc({
                action: "account_balance",
                account: account,
                include_only_confirmed: true
            });

            if (!data || !data.balance || !data.receivable) {
                throw Error("Failed to get balance.");
            }

            return {
                balance: tools.convert(data.balance, 'RAW', 'NANO'),
                receivable: tools.convert(data.receivable, 'RAW', 'NANO'),
            }
        } catch (error) {
            console.error(`Balance check failed (${retries} retries left):`, error);
            retries--;
            if (retries === 0) throw error;
            await sleep(2000); // Wait 2 seconds before retrying
        }
    }
    throw Error("Failed to get balance after all retries");
}

async function createAccount(): Promise<string> {
    // Generate a new wallet
    const walletData = wallet.generate();
    return walletData.accounts[0].address;
}

async function send(destination: string, source: string, amount: string): Promise<string> {
    // Try to get account info, but don't throw if account doesn't exist
    let accountInfo;
    try {
        accountInfo = await rpc({
            action: "account_info",
            account: source,
            representative: true,
            receivable: true,
            include_confirmed: true
        });
    } catch (error) {
        throw Error("Source account must exist and have a balance to send");
    }

    const previous = accountInfo?.frontier;
    const balance = accountInfo?.balance;
    const representative = accountInfo?.representative || destination;

    if (!previous || !balance) {
        throw Error("Source account must exist and have a balance to send");
    }

    // Convert NANO to raw
    const amountRaw = tools.convert(amount, 'NANO', 'RAW');
    const newBalance = (BigInt(balance) - BigInt(amountRaw)).toString();

    if (BigInt(newBalance) < BigInt(0)) {
        throw Error("Insufficient balance");
    }

    // Generate work for the block
    const work = await generateWork(previous);

    // Create the block data
    const blockData = {
        walletBalanceRaw: balance,
        fromAddress: source,
        toAddress: destination,
        representativeAddress: representative,
        frontier: previous,
        amountRaw: amountRaw,
        work: work
    };

    // Sign the block
    const signedBlock = block.send(blockData, process.env.PRIVATE_KEY!);

    // Process the block
    const data = await rpc({
        action: "process",
        json_block: "true",
        subtype: "send",
        block: signedBlock
    });

    if (!data || !data.hash) {
        throw Error("Failed to send nano.");
    }

    console.log("sent " + amount + " nano to " + destination);
    return data.hash;
}

async function receive(account: string, blockHash: string) : Promise<string> {
    // Get pending block info first
    const blockInfo = await rpc({
        action: "blocks_info",
        hashes: [blockHash],
        json_block: "true",
        include_not_found: "true"
    });

    if (!blockInfo || !blockInfo.blocks || !blockInfo.blocks[blockHash]) {
        throw Error("Failed to get block info");
    }

    // Try to get account info, but don't throw if account doesn't exist
    let accountInfo;
    try {
        accountInfo = await rpc({
            action: "account_info",
            account: account,
            representative: true,
            receivable: true,
            include_confirmed: true
        });
    } catch (error) {
        // Account doesn't exist yet, this is fine
        console.log("Account doesn't exist yet, will be opened with this receive");
    }

    const previous = accountInfo?.frontier || "0".repeat(64);
    const representative = accountInfo?.representative || account;
    const currentBalance = accountInfo?.balance || "0";
    const amountRaw = blockInfo.blocks[blockHash].amount;
    const newBalance = (BigInt(currentBalance) + BigInt(amountRaw)).toString();

    // Generate work for the block
    const work = await generateWork(
        previous === "0".repeat(64) ? tools.addressToPublicKey(account) : previous,
        previous === "0".repeat(64)
    );

    // Create the block data
    const blockData = {
        walletBalanceRaw: currentBalance,
        toAddress: account,
        representativeAddress: representative,
        frontier: previous,
        transactionHash: blockHash,
        amountRaw: amountRaw,
        work: work
    };

    // Sign the block
    const signedBlock = block.receive(blockData, process.env.PRIVATE_KEY!);

    // Process the block
    const data = await rpc({
        action: "process",
        json_block: "true",
        subtype: previous === "0".repeat(64) ? "open" : "receive",
        block: signedBlock
    });

    if (!data || !data.hash) {
        throw Error("Failed to receive nano")
    }

    return data.hash;
}

async function receive_all(account: string) {
    const data = await rpc({
        action: "receivable",
        account: account,
        count: "10",
        source: "true",
        include_active: "true",
        include_only_confirmed: "true",
        sorting: "amount",
        threshold: "1"
    });

    if (!data || !data.blocks) {
        return { received: 0 };
    }

    const blocks = Object.keys(data.blocks);
    let received = 0;

    for (const blockHash of blocks) {
        try {
            await receive(account, blockHash);
            received++;
        } catch (error) {
            console.error("Failed to receive block:", error);
        }
    }

    return { received };
}

async function testWalletTransfer() {
    // Initialize logger
    const logger = new Logger(path.join(__dirname, 'logs'));
    
    try {
        logger.log('TEST_START', 'Starting Wallet Transfer Test');

        // Step 1: Create first wallet
        logger.log('WALLET1_CREATE', 'Creating first wallet');
        const wallet1Address = await createAccount();
        
        logger.log('WALLET1_CREATED', {
            address: wallet1Address
        });

        console.log('\n=== WALLET 1 ADDRESS ===');
        console.log('Please send exactly 0.00001 NANO to this address:');
        console.log(wallet1Address);
        console.log('Waiting for funds...');

        // Step 2: Wait and check for incoming transaction
        logger.log('WAITING_FOR_FUNDS', 'Waiting for incoming transaction');
        let funded = false;
        let startTime = Date.now();
        const timeoutMs = 300000; // 5 minutes timeout
        let lastBalance = { balance: '0', receivable: '0' };

        while (!funded && (Date.now() - startTime) < timeoutMs) {
            try {
                const balanceInfo = await balance(wallet1Address);
                
                // Only log if balance or receivable has changed
                if (balanceInfo.balance !== lastBalance.balance || balanceInfo.receivable !== lastBalance.receivable) {
                    logger.log('BALANCE_UPDATE', {
                        account: wallet1Address,
                        balance: balanceInfo.balance,
                        receivable: balanceInfo.receivable
                    });
                    lastBalance = balanceInfo;
                }
                
                if (parseFloat(balanceInfo.balance) > 0 || parseFloat(balanceInfo.receivable) > 0) {
                    logger.log('FUNDS_RECEIVED', {
                        account: wallet1Address,
                        balance: balanceInfo.balance,
                        receivable: balanceInfo.receivable
                    });
                    
                    // Receive any pending funds
                    await receive_all(wallet1Address);
                    funded = true;
                } else {
                    await sleep(5000); // Check every 5 seconds
                }
            } catch (error) {
                logger.logError('CHECK_BALANCE_ERROR', error);
                await sleep(5000);
            }
        }

        if (!funded) {
            throw new Error('Timeout waiting for funds');
        }

        // Step 3: Create second wallet
        logger.log('WALLET2_CREATE', 'Creating second wallet');
        const wallet2Address = await createAccount();
        
        logger.log('WALLET2_CREATED', {
            address: wallet2Address
        });

        // Step 4: Send to wallet2
        logger.log('SENDING_TO_WALLET2', 'Sending funds from wallet 1 to wallet 2');
        const sendAmount = '0.00001';
        const sendHash = await send(wallet2Address, wallet1Address, sendAmount);
        
        logger.log('SENT_TO_WALLET2', {
            hash: sendHash,
            from: wallet1Address,
            to: wallet2Address,
            amount: sendAmount
        });

        // Step 5: Receive in wallet2
        logger.log('RECEIVING_IN_WALLET2', 'Receiving funds in wallet 2');
        await sleep(5000); // Wait for transaction to propagate
        await receive_all(wallet2Address);
        
        // Step 6: Send back to wallet1
        logger.log('SENDING_BACK_TO_WALLET1', 'Sending funds back to wallet 1');
        const sendBackHash = await send(wallet1Address, wallet2Address, sendAmount);
        
        logger.log('SENT_TO_WALLET1', {
            hash: sendBackHash,
            from: wallet2Address,
            to: wallet1Address,
            amount: sendAmount
        });

        // Step 7: Receive back in wallet1
        logger.log('RECEIVING_BACK_IN_WALLET1', 'Receiving funds back in wallet 1');
        await sleep(5000); // Wait for transaction to propagate
        await receive_all(wallet1Address);

        // Test Summary
        const testResults = {
            total: 7,
            passed: 7,
            failed: 0,
            duration: Date.now() - startTime,
            wallet1: {
                address: wallet1Address
            },
            wallet2: {
                address: wallet2Address
            }
        };

        logger.summarize(testResults);
        logger.log('TEST_COMPLETE', 'Wallet Transfer Test completed successfully');

    } catch (error) {
        logger.logError('TEST_FAILURE', error);
        throw error;
    }
}

// Run the test
testWalletTransfer().catch(error => {
    console.error('Test failed:', error);
    process.exit(1);
}); 