type AnyRecord = Record<string, unknown>;

function isRecord(value: unknown): value is AnyRecord {
  return typeof value === "object" && value !== null;
}

function getStringField(obj: AnyRecord, key: string): string | undefined {
  const v = obj[key];
  return typeof v === "string" ? v : undefined;
}

function pickFirstNonEmpty(
  ...values: Array<string | undefined>
): string | undefined {
  for (const v of values) {
    if (v && v.trim().length > 0) return v;
  }
  return;
}

function normalizeMessage(msg: string): string {
  // Common RPC noise: "execution reverted: <reason>".
  const m = msg.replace(/^Error:\s*/i, "").trim();
  return m;
}

function extractRevertReason(msg: string): string | undefined {
  // Examples seen in providers:
  // - "execution reverted: <reason>"
  // - 'Execution Reverted: {"message":"...","data":{"reason":"<reason>"}} ...'
  // - "VM Exception while processing transaction: reverted with reason string '<reason>'"
  // - "... revert <reason>"
  const s = msg;
  const exec = s.match(/execution reverted(?::\s*(.*))?$/i);
  if (exec?.[1]?.trim()) return exec[1].trim();

  const reasonString = s.match(/reverted with reason string ['"]([^'"]+)['"]/i);
  if (reasonString?.[1]?.trim()) return reasonString[1].trim();

  const revert = s.match(/\brevert(?:ed)?(?::|\s)\s*([^\n\r]+)$/i);
  if (revert?.[1]?.trim()) return revert[1].trim();

  return;
}

function extractJsonBlobAfterPrefix(msg: string): unknown | undefined {
  // thirdweb sometimes formats as:
  // "Execution Reverted: { ...json... }\n\ncontract: 0x... \nchainId: 1337"
  const first = msg.indexOf("{");
  if (first === -1) return;

  // If there's metadata after the JSON, trim it out.
  const metaMarkers = [
    "\n\ncontract:",
    "\ncontract:",
    "\n\nchainId:",
    "\nchainId:",
  ];
  let end = msg.length;
  for (const marker of metaMarkers) {
    const idx = msg.indexOf(marker, first);
    if (idx !== -1) end = Math.min(end, idx);
  }

  const slice = msg.slice(first, end);
  const last = slice.lastIndexOf("}");
  if (last === -1) return;

  const jsonText = slice.slice(0, last + 1);
  try {
    return JSON.parse(jsonText) as unknown;
  } catch {
    return;
  }
}

function extractReasonFromJsonBlob(blob: unknown): string | undefined {
  if (!isRecord(blob)) return;

  const topReason = getStringField(blob, "reason");
  if (topReason?.trim()) return topReason.trim();

  const data = blob.data;
  if (isRecord(data)) {
    const dataReason = getStringField(data, "reason");
    if (dataReason?.trim()) return dataReason.trim();
    const dataMsg = getStringField(data, "message");
    // Some providers set message="revert" and reason separately; message is a fallback only.
    if (dataMsg?.trim() && dataMsg.toLowerCase().includes("revert") === false) {
      return dataMsg.trim();
    }
  }

  return;
}

export function formatTxError(err: unknown): string {
  if (!err) return "Transaction failed";

  const candidates: string[] = [];
  const push = (v?: string) => {
    if (v && v.trim().length > 0) candidates.push(v.trim());
  };

  if (err instanceof Error) {
    push(err.message);
  }

  if (isRecord(err)) {
    push(getStringField(err, "shortMessage"));
    push(getStringField(err, "reason"));
    push(getStringField(err, "message"));

    const raw = err.raw;
    if (raw instanceof Error) push(raw.message);
    if (isRecord(raw)) {
      push(getStringField(raw, "shortMessage"));
      push(getStringField(raw, "reason"));
      push(getStringField(raw, "message"));
      const rawData = raw.data;
      if (isRecord(rawData)) {
        push(getStringField(rawData, "reason"));
        push(getStringField(rawData, "message"));
      }
    }

    const cause = err.cause;
    if (cause instanceof Error) push(cause.message);
    if (isRecord(cause)) {
      push(getStringField(cause, "shortMessage"));
      push(getStringField(cause, "reason"));
      push(getStringField(cause, "message"));
    }

    const data = err.data;
    if (isRecord(data)) {
      push(getStringField(data, "message"));
      push(getStringField(data, "reason"));
    }
  }

  const best = pickFirstNonEmpty(...candidates) ?? "Transaction failed";
  const msg = normalizeMessage(best);

  const lower = msg.toLowerCase();
  if (lower.includes("user rejected") || lower.includes("user denied")) {
    return "Transaction was rejected";
  }
  if (lower.includes("insufficient funds")) {
    return "Insufficient funds to complete transaction";
  }

  const jsonBlob = extractJsonBlobAfterPrefix(msg);
  const reasonFromJson = extractReasonFromJsonBlob(jsonBlob);
  const reason = reasonFromJson ?? extractRevertReason(msg);
  if (reason) {
    return `Transaction reverted: ${reason}`;
  }
  if (lower.includes("revert") || lower.includes("reverted")) {
    return "Transaction reverted by contract";
  }

  // Keep it readable on mobile.
  return msg.length > 180 ? `${msg.slice(0, 180)}...` : msg;
}

Graph