/* eslint-disable no-console */
// Worker: decode TxRaw -> { body, auth_info, signatures }.
// Supports two paths:
// 1) Fast path for common messages via decodeKnownAny.
// 2) Dynamic path for all messages via protobufjs Root loaded from protoDir.

import { parentPort, isMainThread } from "node:worker_threads";
import Long from "long";
import { decodeTxRaw } from "@cosmjs/proto-signing";
import { PubKey as PubKeySecp256k1 } from "cosmjs-types/cosmos/crypto/secp256k1/keys.js";
import { loadProtoRootWithProgress, decodeAnyWithRoot } from "./dynamicProto.ts";
import { TxBody, AuthInfo, Tx } from "cosmjs-types/cosmos/tx/v1beta1/tx.js";
import {decodeKnown} from "../../generated/knownMsgs.ts";

if (isMainThread) {
    throw new Error("txWorker must be run as a worker thread");
}

type InitMsg = { type: "init"; protoDir?: string };
type DecodeMsg = { type: "decode"; id: number; txBase64: string };
type InMsg = InitMsg | DecodeMsg;
type OkMsg = { id: number; ok: true; decoded: any };
type ErrMsg = { id: number; ok: false; error: string };

type WorkerOk = { id: number; ok: true; decoded: any };
type WorkerErr = { id: number; ok: false; error: string };

let protoReady = false;
let protoDirPath: string | undefined;
let protoRoot: any | undefined;







let loadInFlight: Promise<void> | null = null;

// lazy loader with mutex
async function ensureProtoLoaded(): Promise<void> {
    if (protoReady) return;
    if (loadInFlight) return loadInFlight;
    loadInFlight = (async () => {
        if (!protoDirPath) {
            console.warn("[txWorker] no protoDir provided — dynamic decode will stay disabled");
            return;
        }
        protoRoot = await loadProtoRootWithProgress(
            protoDirPath,
            (loaded, total) => parentPort!.postMessage({ type: "progress", loaded, total } as ProgressMsg),
            200
        );
        protoReady = true;
        console.log(`[txWorker] loaded proto root from: ${protoDirPath}`);
    })().finally(() => {
        loadInFlight = null;
    });
    return loadInFlight;
}

function base64ToBytes(b64: string): Uint8Array {
    return Uint8Array.from(Buffer.from(b64, "base64"));
}

function decodeMessage(typeUrl: string, value: Uint8Array): any {
    const fast = decodeKnown(typeUrl, value);
    if (fast) return { "@type": typeUrl, ...fast };
    if (protoReady && protoRoot) {
        try {
            return decodeAnyWithRoot(typeUrl, value, protoRoot);
        } catch {
            // fallthrough
        }
    }
    return { "@type": typeUrl, value_b64: Buffer.from(value).toString("base64") };
}

function decodeBodyToShape(bodyBytes: Uint8Array) {
    const body = TxBody.decode(bodyBytes);
    const messages = (body.messages ?? []).map((any) => decodeMessage(any.typeUrl, any.value));
    return {
        messages,
        memo: body.memo ?? "",
        timeout_height: (body.timeoutHeight ?? Long.UZERO).toString(),
        unordered: (body as any).unordered ?? false,
        timeout_timestamp: (body as any).timeoutTimestamp ?? null,
        extension_options: [],
        non_critical_extension_options: [],
    };
}

parentPort!.on("message", async (m: InMsg) => {
    try {
        if (m.type === "init") {
            protoDirPath = m.protoDir;
            // ready immediately; proto will be loaded on first miss
            parentPort!.postMessage({ type: "ready", ok: true } as ReadyMsg);
            return;
        }

        if (m.type === "decode") {
            const id = m.id;
            const txBytes = base64ToBytes(m.txBase64);

            // decode Tx -> TxBody/AuthInfo/Signatures using cosmjs-types
            const { Tx } = await import("cosmjs-types/cosmos/tx/v1beta1/tx.js");
            const tx = Tx.decode(txBytes);

            // If any message is unknown to fast path, load proto root lazily and retry those messages
            let haveMiss = false;
            for (const any of tx.body?.messages ?? []) {
                if (!decodeKnown(any.typeUrl, any.value)) {
                    haveMiss = true;
                    break;
                }
            }
            if (haveMiss) await ensureProtoLoaded();

            const shaped = {
                "@type": "/cosmos.tx.v1beta1.Tx",
                body: decodeBodyToShape(tx.body ? TxBody.encode(tx.body).finish() : new Uint8Array()),
                auth_info: tx.authInfo && (await import("cosmjs-types/cosmos/tx/v1beta1/tx.js")).AuthInfo
                    .toJSON(tx.authInfo), // keep as-is; can normalize later
                signatures: (tx.signatures ?? []).map((sig: Uint8Array) => Buffer.from(sig).toString("base64")),
            };

            parentPort!.postMessage({ id, ok: true, decoded: shaped } as OkMsg);
            return;
        }
    } catch (e: any) {
        const id = (m as any).id ?? -1;
        parentPort!.postMessage({ id, ok: false, error: String(e?.message ?? e) } as ErrMsg);
    }
});






































// ---------- helpers ----------

function coinsToSnake(cs?: Array<{ denom: string; amount: string }>) {
    return (cs ?? []).map((c) => ({ denom: c.denom, amount: c.amount }));
}

function decodeAuthInfoToShape(authBytes: Uint8Array) {
    const ai = AuthInfo.decode(authBytes);

    const signer_infos = (ai.signerInfos ?? []).map((si) => {
        let public_key: any = undefined;
        if (si.publicKey?.typeUrl === "/cosmos.crypto.secp256k1.PubKey") {
            const pk = PubKeySecp256k1.decode(si.publicKey.value);
            public_key = { "@type": "/cosmos.crypto.secp256k1.PubKey", key: Buffer.from(pk.key).toString("base64") };
        } else if (si.publicKey) {
            public_key = { "@type": si.publicKey.typeUrl, value: Buffer.from(si.publicKey.value).toString("base64") };
        }

        let mode_info: any = undefined;
        if (si.modeInfo?.single) mode_info = { single: { mode: "SIGN_MODE_DIRECT" } };
        else if (si.modeInfo?.multi) mode_info = { multi: {} };

        return {
            public_key,
            mode_info,
            sequence: (si.sequence ?? Long.UZERO).toString(),
        };
    });

    const fee = ai.fee
        ? {
            amount: coinsToSnake(ai.fee.amount as any),
            gas_limit: (ai.fee.gasLimit ?? Long.UZERO).toString(),
            payer: ai.fee.payer ?? "",
            granter: ai.fee.granter ?? "",
        }
        : { amount: [], gas_limit: "0", payer: "", granter: "" };

    return { signer_infos, fee, tip: (ai as any).tip ?? null };
}

function decodeTxBase64(base64: string) {
    const txBytes = Buffer.from(base64, "base64");

    // First attempt: TxRaw (fast path)
    let bodyBytes: Uint8Array | undefined;
    let authInfoBytes: Uint8Array | undefined;
    let sigs: Uint8Array[] | undefined;

    try {
        const txRaw = decodeTxRaw(txBytes);
        bodyBytes = txRaw.bodyBytes;
        authInfoBytes = txRaw.authInfoBytes;
        sigs = txRaw.signatures;
    } catch {
        // ignore, we'll try Tx path below
    }

    // If TxRaw path produced empty body, try decoding as full Tx
    if (!bodyBytes || bodyBytes.length === 0) {
        try {
            const full = Tx.decode(txBytes);
            // If this is a real Tx, it will contain parsed body/authInfo directly
            const body = full.body ? TxBody.encode(full.body).finish() : new Uint8Array();
            const auth = full.authInfo ? AuthInfo.encode(full.authInfo).finish() : new Uint8Array();
            bodyBytes = body;
            authInfoBytes = auth;
            sigs = (full.signatures ?? []) as unknown as Uint8Array[];
        } catch {
            // fall through; we'll log a diagnostic below
        }
    }

    // Diagnostics: if still no body, print a short hex prefix to help debugging
    if (!bodyBytes || bodyBytes.length === 0 || !authInfoBytes) {
        const hexPrefix = Buffer.from(txBytes.slice(0, 8)).toString("hex").toUpperCase();
        // eslint-disable-next-line no-console
        console.warn(`[txWorker] cannot decode body/auth; len=${txBytes.length}, head=${hexPrefix}`);
        // Produce minimal object to avoid crashing the pipeline
        return {
            "@type": "/cosmos.tx.v1beta1.Tx",
            body: { messages: [], memo: "", timeout_height: "0", unordered: false, timeout_timestamp: null, extension_options: [], non_critical_extension_options: [] },
            auth_info: { signer_infos: [], fee: { amount: [], gas_limit: "0", payer: "", granter: "" }, tip: null },
            signatures: [],
        };
    }

    const body = decodeBodyToShape(bodyBytes);
    const auth_info = decodeAuthInfoToShape(authInfoBytes);
    const signatures = (sigs ?? []).map((s) => Buffer.from(s).toString("base64"));

    return {
        "@type": "/cosmos.tx.v1beta1.Tx",
        body,
        auth_info,
        signatures,
    };
}

// ---------- message handling ----------

type ProgressMsg = { type: "progress"; loaded: number; total: number };
type ReadyMsg = { type: "ready"; ok: boolean; detail?: string };
type OutMsg = ProgressMsg | ReadyMsg | { id: number; ok: true; decoded: any } | { id: number; ok: false; error: string };

async function onInit(msg: InitMsg) {
    const protoDirPath = msg.protoDir;
    if (protoDirPath) {
        try {
            // send incremental progress while loading
            protoRoot = await loadProtoRootWithProgress(
                protoDirPath,
                (loaded, total) => parentPort!.postMessage({ type: "progress", loaded, total } as ProgressMsg),
                200 // batch size (tweak if needed)
            );
            protoReady = true;
            console.log(`[txWorker] loaded proto root from: ${protoDirPath}`);

            const toCheck = [
                "cosmos.bank.v1beta1.MsgSend",
                "ibc.applications.transfer.v1.MsgTransfer",
                "cosmos.distribution.v1beta1.MsgWithdrawDelegatorReward",
            ];
            // for (const fullName of toCheck) {
            //     const ok = !!protoRoot.lookupType(fullName);
            //     console.log(`[txWorker] type ${fullName} present: ${ok}`);
            // }

            parentPort!.postMessage({ type: "ready", ok: true } as ReadyMsg);
            return;
        } catch (e: any) {
            console.warn(`[txWorker] failed to load proto root: ${String(e?.message ?? e)}`);
            protoReady = false;
            protoRoot = undefined;
            parentPort!.postMessage({ type: "ready", ok: false, detail: String(e?.message ?? e) } as ReadyMsg);
            return;
        }
    } else {
        console.warn(`[txWorker] no protoDir provided — dynamic decode disabled`);
        parentPort!.postMessage({ type: "ready", ok: true } as ReadyMsg);
        return;
    }
}

function onDecode(msg: DecodeMsg) {
    try {
        const decoded = decodeTxBase64(msg.txBase64);
        const out: WorkerOk = { id: msg.id, ok: true, decoded };
        parentPort!.postMessage(out);
    } catch (e: any) {
        const out: WorkerErr = { id: msg.id, ok: false, error: String(e?.message ?? e) };
        parentPort!.postMessage(out);
    }
}

parentPort!.on("message", (msg: InMsg) => {
    if (msg.type === "init") {
        onInit(msg);
        return;
    }
    if (msg.type === "decode") {
        onDecode(msg);
        return;
    }
});