/* eslint-disable no-console */
import type { AbciEvent, AbciEventAttr } from "../types.js";

/** Returns true if a string "looks like" base64 (len%4=0 and allowed charset) */
export function looksLikeBase64(s: string): boolean {
    if (!s || s.length % 4 !== 0) return false;
    // allow = only at the end; keep it simple and permissive
    return /^[A-Za-z0-9+/=]+$/.test(s);
}

/** Best-effort Base64 -> UTF-8; decodes only if input is canonical base64 AND UTF-8 is valid */
export function tryB64ToUtf8(s: string): string {
    if (!isCanonicalBase64(s)) return s;
    try {
        const bytes = Buffer.from(s, "base64");
        // use fatal decoder: throw if invalid UTF-8 (prevents � replacement chars)
        const decoder = new TextDecoder("utf-8", { fatal: true });
        const txt = decoder.decode(bytes);
        // accept only printable + common whitespace; reject control noise
        if (!/^[\x09\x0A\x0D\x20-\x7E\u0080-\uFFFF]*$/.test(txt)) return s;
        return txt;
    } catch {
        return s;
    }
}

/** Normalizes a single attribute: decode only when safe; always ensure index:true */
export function normalizeAttr(a: any): AbciEventAttr {
    let key = a?.key ?? "";
    let value = a?.value ?? "";
    key = tryB64ToUtf8(String(key));
    value = tryB64ToUtf8(String(value));
    const index = a?.index ?? true;
    return { key, value, index };
}

/** Normalizes a single ABCI event: stable {type, attributes[]} with index:true everywhere */
export function normalizeEvent(e: any): AbciEvent {
    const type = String(e?.type ?? "");
    const attributesIn = Array.isArray(e?.attributes) ? e.attributes : [];
    const attributes = attributesIn.map(normalizeAttr);
    return { type, attributes };
}

/** Normalizes an array of ABCI events (safe for null/undefined input) */
export function normalizeEvents(evs?: any[]): AbciEvent[] {
    if (!Array.isArray(evs)) return [];
    return evs.map(normalizeEvent);
}

/**
 * Parses `raw_log` (ABCI JSON string) into structured logs:
 *   raw_log: string -> [{ msg_index, events: AbciEvent[] }, ...]
 * If raw_log is not a JSON array, returns [].
 * All event attributes are normalized via normalizeEvents().
 */
export function parseRawLogToStructured(rawLog?: string): Array<{ msg_index: number; events: AbciEvent[] }> {
    if (!rawLog) return [];
    try {
        const parsed = JSON.parse(rawLog);
        if (!Array.isArray(parsed)) return [];
        return parsed.map((entry) => {
            const msgIdx = Number(entry?.msg_index ?? entry?.msgIndex ?? 0);
            const events = normalizeEvents(entry?.events);
            return { msg_index: Number.isFinite(msgIdx) ? msgIdx : 0, events };
        });
    } catch {
        // when raw_log is not JSON, we do not error — just return an empty list
        return [];
    }
}

/**
 * Builds the combined `logs` array like explorers do:
 * - message-level logs parsed from `raw_log`
 * - plus a last entry with `msg_index: null` containing tx-level events
 *   (those come from `txs_results[i].events`)
 */
export function buildCombinedLogs(rawLog: string | undefined, txLevelEvents: any[] | undefined) {
    const msgLevel = parseRawLogToStructured(rawLog);
    const txLevel = normalizeEvents(txLevelEvents);
    const combined: Array<{ msg_index: number | null; events: AbciEvent[] }> = [...msgLevel];
    if (txLevel.length > 0) {
        combined.push({ msg_index: null, events: txLevel });
    }
    return combined;
}

/** Returns true if `s` is canonical Base64: length%4==0, charset ok, and re-encoding matches exactly */
export function isCanonicalBase64(s: string): boolean {
    if (!s || s.length % 4 !== 0) return false;
    // must be only base64 charset with up to two '=' paddings at the end
    if (!/^[A-Za-z0-9+/]+={0,2}$/.test(s)) return false;
    try {
        const buf = Buffer.from(s, "base64");
        // re-encode must equal the original exactly (canonical form)
        const recoded = buf.toString("base64");
        return recoded === s;
    } catch {
        return false;
    }
}
