// AutistMask background service worker
// Handles EIP-1193 RPC requests from content scripts and proxies
// non-sensitive calls to the configured Ethereum JSON-RPC endpoint.

const { DEFAULT_RPC_URL } = require("../shared/constants");
const { SUPPORTED_CHAIN_IDS, networkByChainId } = require("../shared/networks");
const { onChainSwitch } = require("../shared/chainSwitch");
const {
    state,
    loadState,
    saveState,
    currentNetwork,
} = require("../shared/state");
const { refreshBalances, getProvider } = require("../shared/balances");
const { debugFetch } = require("../shared/log");
const { verifySignedTx, verifySignature } = require("../shared/approvalVerify");
const {
    isPhishingDomain,
    updatePhishingList,
    startPeriodicRefresh,
} = require("../shared/phishingDomains");

const storageApi =
    typeof browser !== "undefined"
        ? browser.storage.local
        : chrome.storage.local;
const runtime =
    typeof browser !== "undefined" ? browser.runtime : chrome.runtime;
const windowsApi =
    typeof browser !== "undefined" ? browser.windows : chrome.windows;
const tabsApi = typeof browser !== "undefined" ? browser.tabs : chrome.tabs;
const actionApi =
    typeof browser !== "undefined" ? browser.browserAction : chrome.action;

// Connected sites (in-memory, non-persisted): { "origin:address": true }
const connectedSites = {};

// Pending approval requests: { id: { origin, hostname, resolve } }
const pendingApprovals = {};

async function getState() {
    const result = await storageApi.get("autistmask");
    return (
        result.autistmask || {
            wallets: [],
            rpcUrl: DEFAULT_RPC_URL,
            activeAddress: null,
            allowedSites: {},
            deniedSites: {},
        }
    );
}

async function getActiveAddress() {
    const s = await getState();
    if (s.activeAddress) return s.activeAddress;
    // Fall back to first address
    if (s.wallets.length > 0 && s.wallets[0].addresses.length > 0) {
        return s.wallets[0].addresses[0].address;
    }
    return null;
}

async function getRpcUrl() {
    const s = await getState();
    return s.rpcUrl || DEFAULT_RPC_URL;
}

function extractHostname(origin) {
    try {
        return new URL(origin).hostname;
    } catch {
        return origin;
    }
}

// Proxy an RPC call to the Ethereum node
async function proxyRpc(method, params) {
    const rpcUrl = await getRpcUrl();
    const resp = await debugFetch(rpcUrl, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
            jsonrpc: "2.0",
            id: 1,
            method,
            params,
        }),
    });
    const json = await resp.json();
    if (json.error) {
        throw new Error(json.error.message || "RPC error");
    }
    return json.result;
}

function resetPopupUrl() {
    if (actionApi && typeof actionApi.setPopup === "function") {
        actionApi.setPopup({ popup: "src/popup/index.html" });
    }
}

// Open approval in a separate popup window.
// This is the primary mechanism for tx/sign approvals (triggered programmatically,
// not from a user gesture) and the fallback for site-connection approvals.
function openApprovalWindow(id) {
    const popupUrl = runtime.getURL("src/popup/index.html?approval=" + id);
    const popupWidth = 360;
    const popupHeight = 600;

    windowsApi.getLastFocused((currentWin) => {
        const opts = {
            url: popupUrl,
            type: "popup",
            width: popupWidth,
            height: popupHeight,
        };
        if (currentWin) {
            opts.left = Math.round(
                currentWin.left + (currentWin.width - popupWidth) / 2,
            );
            opts.top = Math.round(
                currentWin.top + (currentWin.height - popupHeight) / 2,
            );
        }
        windowsApi.create(opts, (win) => {
            if (win) {
                pendingApprovals[id].windowId = win.id;
            }
        });
    });
}

// Open an approval popup and return a promise that resolves with the user decision.
// Prefers the browser-action popup (anchored to toolbar, no macOS Space switch).
function requestApproval(origin, hostname) {
    return new Promise((resolve) => {
        const id = crypto.randomUUID();
        pendingApprovals[id] = { origin, hostname, resolve };

        if (actionApi && typeof actionApi.openPopup === "function") {
            actionApi.setPopup({
                popup: "src/popup/index.html?approval=" + id,
            });
            try {
                const result = actionApi.openPopup();
                if (result && typeof result.catch === "function") {
                    result.catch(() => openApprovalWindow(id));
                }
            } catch {
                openApprovalWindow(id);
            }
        } else {
            openApprovalWindow(id);
        }
    });
}

// Open a tx-approval popup and return a promise that resolves with txHash or error.
// Uses windows.create() directly because tx approvals are triggered programmatically
// (from a dApp RPC call), not from a user gesture, so action.openPopup() is
// unreliable in this context.
function requestTxApproval(origin, hostname, txParams) {
    return new Promise((resolve) => {
        const id = crypto.randomUUID();
        pendingApprovals[id] = {
            origin,
            hostname,
            txParams,
            resolve,
            type: "tx",
        };

        openApprovalWindow(id);
    });
}

// Open a sign-approval popup and return a promise that resolves with { signature } or { error }.
// Uses windows.create() directly because sign approvals are triggered programmatically
// (from a dApp RPC call), not from a user gesture, so action.openPopup() is
// unreliable in this context.
function requestSignApproval(origin, hostname, signParams) {
    return new Promise((resolve) => {
        const id = crypto.randomUUID();
        pendingApprovals[id] = {
            origin,
            hostname,
            signParams,
            resolve,
            type: "sign",
        };

        openApprovalWindow(id);
    });
}

// Detect when an approval popup (browser-action) closes without a response.
// TX and sign approvals now use windows.create() and are handled by the
// windowsApi.onRemoved listener below, but we still handle site-connection
// approval disconnects here.
runtime.onConnect.addListener((port) => {
    if (port.name.startsWith("approval:")) {
        const id = port.name.split(":")[1];
        port.onDisconnect.addListener(() => {
            const approval = pendingApprovals[id];
            if (approval) {
                if (approval.type === "tx" || approval.type === "sign") {
                    // Keep pending — user can reopen the toolbar popup
                    return;
                }
                approval.resolve({ approved: false, remember: false });
                delete pendingApprovals[id];
            }
            resetPopupUrl();
        });
    }
});

// Handle connection requests (eth_requestAccounts, wallet_requestPermissions)
async function handleConnectionRequest(origin) {
    const s = await getState();
    const activeAddress = await getActiveAddress();
    if (!activeAddress) {
        return { error: { message: "No accounts available" } };
    }

    const hostname = extractHostname(origin);
    const allowed = s.allowedSites[activeAddress] || [];
    const denied = s.deniedSites[activeAddress] || [];

    // Check denied list
    if (denied.includes(hostname)) {
        return {
            error: {
                code: 4001,
                message: "User rejected the request.",
            },
        };
    }

    // Check allowed list or in-memory connected
    if (
        allowed.includes(hostname) ||
        connectedSites[origin + ":" + activeAddress]
    ) {
        return { result: [activeAddress] };
    }

    // Open approval popup
    const decision = await requestApproval(origin, hostname);

    if (decision.approved) {
        if (decision.remember) {
            // Reload state to get latest, add to allowed, persist
            await loadState();
            if (!state.allowedSites[activeAddress]) {
                state.allowedSites[activeAddress] = [];
            }
            if (!state.allowedSites[activeAddress].includes(hostname)) {
                state.allowedSites[activeAddress].push(hostname);
            }
            await saveState();
        } else {
            connectedSites[origin + ":" + activeAddress] = true;
        }
        return { result: [activeAddress] };
    } else {
        if (decision.remember) {
            await loadState();
            if (!state.deniedSites[activeAddress]) {
                state.deniedSites[activeAddress] = [];
            }
            if (!state.deniedSites[activeAddress].includes(hostname)) {
                state.deniedSites[activeAddress].push(hostname);
            }
            await saveState();
        }
        return {
            error: {
                code: 4001,
                message: "User rejected the request.",
            },
        };
    }
}

// Methods that are safe to proxy directly to the RPC node
const PROXY_METHODS = [
    "eth_blockNumber",
    "eth_call",
    "eth_chainId",
    "eth_estimateGas",
    "eth_gasPrice",
    "eth_getBalance",
    "eth_getBlockByHash",
    "eth_getBlockByNumber",
    "eth_getCode",
    "eth_getLogs",
    "eth_getStorageAt",
    "eth_getTransactionByHash",
    "eth_getTransactionCount",
    "eth_getTransactionReceipt",
    "eth_maxPriorityFeePerGas",
    "eth_sendRawTransaction",
    "net_version",
    "web3_clientVersion",
    "eth_feeHistory",
    "eth_getBlockTransactionCountByHash",
    "eth_getBlockTransactionCountByNumber",
];

async function handleRpc(method, params, origin) {
    // Connection requests — go through approval flow
    if (method === "eth_requestAccounts") {
        return handleConnectionRequest(origin);
    }

    if (method === "eth_accounts") {
        const s = await getState();
        const activeAddress = await getActiveAddress();
        if (!activeAddress) return { result: [] };
        const hostname = extractHostname(origin);
        const allowed = s.allowedSites[activeAddress] || [];
        if (
            allowed.includes(hostname) ||
            connectedSites[origin + ":" + activeAddress]
        ) {
            return { result: [activeAddress] };
        }
        return { result: [] };
    }

    if (method === "eth_chainId") {
        return { result: currentNetwork().chainId };
    }

    if (method === "net_version") {
        return { result: currentNetwork().networkVersion };
    }

    if (method === "wallet_switchEthereumChain") {
        const chainId = params?.[0]?.chainId;
        if (chainId === currentNetwork().chainId) {
            return { result: null };
        }
        if (SUPPORTED_CHAIN_IDS.has(chainId)) {
            const target = networkByChainId(chainId);
            await onChainSwitch(target.id);
            broadcastChainChanged(target.chainId);
            return { result: null };
        }
        return {
            error: {
                code: 4902,
                message:
                    "AutistMask supports Ethereum Mainnet and Sepolia Testnet only.",
            },
        };
    }

    if (method === "wallet_addEthereumChain") {
        const chainId = params?.[0]?.chainId;
        if (SUPPORTED_CHAIN_IDS.has(chainId)) {
            return { result: null };
        }
        return {
            error: {
                code: 4902,
                message:
                    "AutistMask supports Ethereum Mainnet and Sepolia Testnet only.",
            },
        };
    }

    if (method === "wallet_requestPermissions") {
        const connResult = await handleConnectionRequest(origin);
        if (connResult.error) return connResult;
        return {
            result: [
                {
                    parentCapability: "eth_accounts",
                    caveats: [
                        {
                            type: "restrictReturnedAccounts",
                            value: connResult.result,
                        },
                    ],
                },
            ],
        };
    }

    if (method === "wallet_getPermissions") {
        const s = await getState();
        const activeAddress = await getActiveAddress();
        const hostname = extractHostname(origin);
        const allowed = s.allowedSites[activeAddress] || [];
        const isConnected =
            allowed.includes(hostname) ||
            connectedSites[origin + ":" + activeAddress];
        if (!isConnected || !activeAddress) {
            return { result: [] };
        }
        return {
            result: [
                {
                    parentCapability: "eth_accounts",
                    caveats: [
                        {
                            type: "restrictReturnedAccounts",
                            value: [activeAddress],
                        },
                    ],
                },
            ],
        };
    }

    if (method === "personal_sign" || method === "eth_sign") {
        const s = await getState();
        const activeAddress = await getActiveAddress();
        if (!activeAddress)
            return { error: { message: "No accounts available" } };

        const hostname = extractHostname(origin);
        const allowed = s.allowedSites[activeAddress] || [];
        if (
            !allowed.includes(hostname) &&
            !connectedSites[origin + ":" + activeAddress]
        ) {
            return { error: { code: 4100, message: "Unauthorized" } };
        }

        // personal_sign: params[0]=message, params[1]=address
        // eth_sign:      params[0]=address, params[1]=message
        const signParams =
            method === "personal_sign"
                ? { method, message: params[0], from: params[1] }
                : { method, message: params[1], from: params[0] };

        if (method === "eth_sign") {
            signParams.dangerWarning =
                "\u26a0\ufe0f DANGER: This site is requesting to sign a raw hash. " +
                "This can be used to sign transactions that drain your funds. " +
                "Only proceed if you fully understand what you are signing.";
        }

        const decision = await requestSignApproval(
            origin,
            hostname,
            signParams,
        );
        if (decision.error) return { error: decision.error };
        return { result: decision.signature };
    }

    if (method === "eth_signTypedData_v4" || method === "eth_signTypedData") {
        const s = await getState();
        const activeAddress = await getActiveAddress();
        if (!activeAddress)
            return { error: { message: "No accounts available" } };

        const hostname = extractHostname(origin);
        const allowed = s.allowedSites[activeAddress] || [];
        if (
            !allowed.includes(hostname) &&
            !connectedSites[origin + ":" + activeAddress]
        ) {
            return { error: { code: 4100, message: "Unauthorized" } };
        }

        const signParams = { method, typedData: params[1], from: params[0] };
        const decision = await requestSignApproval(
            origin,
            hostname,
            signParams,
        );
        if (decision.error) return { error: decision.error };
        return { result: decision.signature };
    }

    if (method === "eth_sendTransaction") {
        const s = await getState();
        const activeAddress = await getActiveAddress();
        if (!activeAddress)
            return { error: { message: "No accounts available" } };

        const hostname = extractHostname(origin);
        const allowed = s.allowedSites[activeAddress] || [];
        if (
            !allowed.includes(hostname) &&
            !connectedSites[origin + ":" + activeAddress]
        ) {
            return { error: { code: 4100, message: "Unauthorized" } };
        }

        const txParams = params?.[0] || {};
        const decision = await requestTxApproval(origin, hostname, txParams);
        if (decision.error) return { error: decision.error };
        return { result: decision.txHash };
    }

    // Proxy safe read-only methods to the RPC node
    if (PROXY_METHODS.includes(method)) {
        try {
            const result = await proxyRpc(method, params);
            return { result };
        } catch (e) {
            return { error: { message: e.message } };
        }
    }

    return { error: { message: "Unsupported method: " + method } };
}

// Broadcast chainChanged to all tabs when the network is switched.
function broadcastChainChanged(chainId) {
    tabsApi.query({}, (tabs) => {
        for (const tab of tabs) {
            tabsApi.sendMessage(
                tab.id,
                {
                    type: "AUTISTMASK_EVENT",
                    eventName: "chainChanged",
                    data: chainId,
                },
                () => {
                    if (runtime.lastError) {
                        // expected for tabs without our content script
                    }
                },
            );
        }
    });
}

// Broadcast accountsChanged to all tabs, respecting per-address permissions
async function broadcastAccountsChanged() {
    // Clear non-remembered approvals on address switch
    for (const key of Object.keys(connectedSites)) {
        delete connectedSites[key];
    }
    // Reject and close any pending approval popups so they don't hang
    for (const [id, approval] of Object.entries(pendingApprovals)) {
        if (approval.type === "tx" || approval.type === "sign") {
            approval.resolve({
                error: { code: 4001, message: "User rejected the request." },
            });
        } else {
            approval.resolve({ approved: false, remember: false });
        }
        if (approval.windowId) {
            windowsApi.remove(approval.windowId, () => {
                if (runtime.lastError) {
                    // window already closed
                }
            });
        }
        delete pendingApprovals[id];
    }
    resetPopupUrl();
    const s = await getState();
    const activeAddress = await getActiveAddress();
    const allowed = activeAddress ? s.allowedSites[activeAddress] || [] : [];
    tabsApi.query({}, (tabs) => {
        for (const tab of tabs) {
            const origin = tab.url ? new URL(tab.url).origin : "";
            const hostname = extractHostname(origin);
            const hasPermission =
                activeAddress &&
                (allowed.includes(hostname) ||
                    connectedSites[origin + ":" + activeAddress]);
            tabsApi.sendMessage(
                tab.id,
                {
                    type: "AUTISTMASK_EVENT",
                    eventName: "accountsChanged",
                    data: hasPermission ? [activeAddress] : [],
                },
                () => {
                    // Ignore errors for tabs without content script
                    if (runtime.lastError) {
                        // expected for tabs without our content script
                    }
                },
            );
        }
    });
}

// Background balance refresh: every 60 seconds when the popup isn't open.
// When the popup IS open, its 10-second interval keeps lastBalanceRefresh
// fresh, so this naturally skips.
const BACKGROUND_REFRESH_INTERVAL = 60000;

async function backgroundRefresh() {
    await loadState();
    const now = Date.now();
    if (now - (state.lastBalanceRefresh || 0) < BACKGROUND_REFRESH_INTERVAL)
        return;
    if (state.wallets.length === 0) return;
    await refreshBalances(
        state.wallets,
        state.rpcUrl,
        state.blockscoutUrl,
        state.trackedTokens,
    );
    state.lastBalanceRefresh = now;
    await saveState();
}

setInterval(backgroundRefresh, BACKGROUND_REFRESH_INTERVAL);

// Fetch the phishing domain blocklist delta on startup and refresh every 24h.
// The vendored blocklist is bundled at build time; this fetches only new entries.
updatePhishingList();
startPeriodicRefresh();

// When approval window is closed without a response, treat as rejection
if (windowsApi && windowsApi.onRemoved) {
    windowsApi.onRemoved.addListener((windowId) => {
        for (const [id, approval] of Object.entries(pendingApprovals)) {
            if (approval.windowId === windowId) {
                if (approval.type === "tx" || approval.type === "sign") {
                    approval.resolve({
                        error: {
                            code: 4001,
                            message: "User rejected the request.",
                        },
                    });
                } else {
                    approval.resolve({ approved: false, remember: false });
                }
                delete pendingApprovals[id];
            }
        }
    });
}

// Listen for messages from content scripts and popup
runtime.onMessage.addListener((msg, sender, sendResponse) => {
    if (msg.type === "AUTISTMASK_RPC") {
        // Derive origin from trusted sender info to prevent origin spoofing.
        // Chrome MV3 provides sender.origin; Firefox MV2 fallback uses sender.tab.url.
        let trustedOrigin = msg.origin; // fallback only if sender info unavailable
        if (sender.origin) {
            trustedOrigin = sender.origin;
        } else if (sender.tab && sender.tab.url) {
            try {
                trustedOrigin = new URL(sender.tab.url).origin;
            } catch {
                // keep fallback
            }
        }
        handleRpc(msg.method, msg.params, trustedOrigin).then((response) => {
            sendResponse(response);
        });
        return true;
    }

    // Validate that popup-only messages originate from the extension itself.
    const POPUP_ONLY_TYPES = [
        "AUTISTMASK_GET_APPROVAL",
        "AUTISTMASK_APPROVAL_RESPONSE",
        "AUTISTMASK_TX_RESPONSE",
        "AUTISTMASK_SIGN_RESPONSE",
    ];
    if (POPUP_ONLY_TYPES.includes(msg.type)) {
        const extUrl = runtime.getURL("");
        if (!sender.url || !sender.url.startsWith(extUrl)) {
            sendResponse({ error: "Unauthorized sender" });
            return false;
        }
    }

    if (msg.type === "AUTISTMASK_GET_APPROVAL") {
        const approval = pendingApprovals[msg.id];
        if (approval) {
            const resp = {
                hostname: approval.hostname,
                origin: approval.origin,
            };
            if (approval.type === "tx") {
                resp.type = "tx";
                resp.txParams = approval.txParams;
            }
            if (approval.type === "sign") {
                resp.type = "sign";
                resp.signParams = approval.signParams;
            }
            // Flag if the requesting domain is on the phishing blocklist.
            resp.isPhishingDomain = isPhishingDomain(approval.hostname);
            sendResponse(resp);
        } else {
            sendResponse(null);
        }
        return false;
    }

    if (msg.type === "AUTISTMASK_APPROVAL_RESPONSE") {
        const approval = pendingApprovals[msg.id];
        if (approval) {
            approval.resolve({
                approved: msg.approved,
                remember: msg.remember,
            });
            delete pendingApprovals[msg.id];
        }
        resetPopupUrl();
        return false;
    }

    if (msg.type === "AUTISTMASK_TX_RESPONSE") {
        const approval = pendingApprovals[msg.id];
        if (!approval) return false;
        delete pendingApprovals[msg.id];
        resetPopupUrl();

        if (!msg.approved) {
            approval.resolve({
                error: { code: 4001, message: "User rejected the request." },
            });
            return true;
        }

        // The popup signs; it reports back here when it could not. Fail the
        // request the same way this handler used to when it did the signing.
        if (msg.error) {
            approval.resolve({ error: { message: msg.error } });
            sendResponse({ error: msg.error });
            return false;
        }

        (async () => {
            try {
                await loadState();
                const activeAddress = await getActiveAddress();
                // The popup holds the secret, but the background stays the
                // authority on what is broadcast: the raw transaction must be
                // the approved one, signed by the approved address.
                verifySignedTx(
                    msg.rawSignedTx,
                    approval.txParams,
                    activeAddress,
                );
                const provider = getProvider(state.rpcUrl);
                const tx = await provider.broadcastTransaction(msg.rawSignedTx);
                approval.resolve({ txHash: tx.hash });
                sendResponse({ txHash: tx.hash });
            } catch (e) {
                const errMsg = e.shortMessage || e.message;
                approval.resolve({
                    error: { message: errMsg },
                });
                sendResponse({ error: errMsg });
            }
        })();
        return true;
    }

    if (msg.type === "AUTISTMASK_SIGN_RESPONSE") {
        const approval = pendingApprovals[msg.id];
        if (!approval) return false;
        delete pendingApprovals[msg.id];
        resetPopupUrl();

        if (!msg.approved) {
            approval.resolve({
                error: { code: 4001, message: "User rejected the request." },
            });
            return true;
        }

        // The popup signs; it reports back here when it could not. Fail the
        // request the same way this handler used to when it did the signing.
        if (msg.error) {
            approval.resolve({ error: { message: msg.error } });
            sendResponse({ error: msg.error });
            return false;
        }

        (async () => {
            try {
                const activeAddress = await getActiveAddress();
                // The popup holds the secret, but the background stays the
                // authority on what is handed back to the page: the signature
                // must cover the approved payload and recover to the approved
                // address.
                const signature = msg.signature;
                verifySignature(approval.signParams, signature, activeAddress);
                approval.resolve({ signature });
                sendResponse({ signature });
            } catch (e) {
                const errMsg = e.shortMessage || e.message;
                approval.resolve({
                    error: { message: errMsg },
                });
                sendResponse({ error: errMsg });
            }
        })();
        return true;
    }

    if (msg.type === "AUTISTMASK_ACTIVE_CHANGED") {
        broadcastAccountsChanged();
        return false;
    }

    if (msg.type === "AUTISTMASK_REMOVE_SITE") {
        // Popup already saved state; nothing else needed
        return false;
    }
});
