import {
  ExchangeClient,
  HttpTransport,
  type IRequestTransport,
} from "@hfunlabs/hyperliquid";
import {
  signUserSignedAction,
  type AbstractViemLocalAccount,
} from "@hfunlabs/hyperliquid/signing";
import type { RpcOptions } from "@protobuf-ts/runtime-rpc";
import type { TelegramUserResponse } from "hypurr-grpc/ts/hypurr/telegram/telegram_service";
import type {
  TelegramUser as HypurrTelegramUser,
  TelegramChatWalletPack,
} from "hypurr-grpc/ts/hypurr/user";
import type { HyperliquidWallet } from "hypurr-grpc/ts/hypurr/wallet";
import {
  createContext,
  useCallback,
  useContext,
  useEffect,
  useMemo,
  useRef,
  useState,
  type ReactNode,
} from "react";
import {
  AGENT_NAME,
  clearAgent as clearStoredAgent,
  fetchAgentByAddress,
  fetchActiveAgent,
  generateAgentKey,
  isAgentValid,
  isDeadAgentError,
  loadAgent,
  saveAgent,
} from "./agent";
import { createStaticClient, createTelegramClient } from "./grpc";
import { GrpcExchangeTransport } from "./GrpcExchangeTransport";
import { PrivateKeySigner } from "./privateKeySigner";
import { createTelegramAgentApprovalName } from "./agentWallet";
import type {
  AuthMethod,
  EoaSigner,
  EvmTransactionRequest,
  Hex,
  HypurrConnectConfig,
  HypurrConnectState,
  HypurrUser,
  RenewAgentWalletParams,
  SignTypedDataFn,
  StoredAgent,
} from "./types";

/** @internal context value — extends the public type with fields used only by library internals */
interface InternalConnectState extends HypurrConnectState {
  loginTelegram: () => void;
  /** Connected EOA owner address (EOA auth only); null otherwise. */
  eoaAddress: `0x${string}` | null;
}

const TELEGRAM_STORAGE_KEY = "hypurr-connect-tg-jwt";
const LEGACY_TELEGRAM_STORAGE_KEY = "hypurr-connect-tg-user";
const TELEGRAM_AUTH_STATE_KEY = "hypurr-connect-auth-state";
const TELEGRAM_AUTH_CODE_VERIFIER_PREFIX = "hypurr-connect-auth-code-verifier:";
const TELEGRAM_AUTH_RETURN_TO_PREFIX = "hypurr-connect-auth-return-to:";
const TELEGRAM_AUTH_MESSAGE = "hypurr-connect:telegram-auth";
const DEFAULT_AUTH_HUB_URL = "https://auth.hypurr.fun/login";
const DEFAULT_MEDIA_URL = "https://media.hypurr.fun";
const USER_SIGNED_DOMAIN_NAME = "HyperliquidSignTransaction";
const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" as const;
const APPROVE_AGENT_PRIMARY_TYPE = "HyperliquidTransaction:ApproveAgent";
const APPROVE_AGENT_TYPES = {
  [APPROVE_AGENT_PRIMARY_TYPE]: [
    { name: "hyperliquidChain", type: "string" },
    { name: "agentAddress", type: "address" },
    { name: "agentName", type: "string" },
    { name: "nonce", type: "uint64" },
  ],
};
const EIP712_DOMAIN_TYPES = [
  { name: "name", type: "string" },
  { name: "version", type: "string" },
  { name: "chainId", type: "uint256" },
  { name: "verifyingContract", type: "address" },
];
const IGNORED_EXTERNAL_SIGNATURE =
  "0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001b" as const;
const DEFAULT_TELEGRAM_SCOPES = [
  "telegram:user:read",
  "telegram:wallet:read",
  "telegram:wallet:write",
  "telegram:trade:read",
  "telegram:trade:write",
  "telegram:cabal:read",
  "telegram:cabal:write",
  "telegram:agent:write",
  "telegram:support:read",
  "telegram:support:write",
];

function createExternalSigningWallet(address: Hex): AbstractViemLocalAccount {
  return {
    address,
    signTypedData(params) {
      void params;
      return Promise.resolve(IGNORED_EXTERNAL_SIGNATURE);
    },
  };
}

function isInvalidTelegramAuthError(err: unknown): boolean {
  const msg =
    err instanceof Error
      ? err.message
      : typeof err === "object" && err !== null && "message" in err
        ? String((err as { message: unknown }).message)
        : String(err);
  return /invalid telegram auth data|invalid auth token|missing authorization token/i.test(
    msg,
  );
}

function normalizeMediaUrl(mediaUrl?: string): string {
  return (mediaUrl?.trim() || DEFAULT_MEDIA_URL).replace(/\/+$/, "");
}

function isAddress(value?: string | null): value is Hex {
  return !!value && /^0x[a-fA-F0-9]{40}$/.test(value);
}

function createApproveAgentAction(params: {
  agentAddress: Hex;
  agentName: string;
  chainId: number;
  isTestnet: boolean;
}) {
  const nonce = Date.now();
  return {
    action: {
      type: "approveAgent" as const,
      signatureChainId: `0x${params.chainId.toString(16)}` as Hex,
      hyperliquidChain: (params.isTestnet ? "Testnet" : "Mainnet") as
        | "Testnet"
        | "Mainnet",
      agentAddress: params.agentAddress.toLowerCase() as Hex,
      agentName: params.agentName,
      nonce,
    },
    nonce,
  };
}

type ApproveAgentAction = ReturnType<typeof createApproveAgentAction>["action"];

async function signApproveAgentAction(params: {
  signTypedDataAsync: SignTypedDataFn;
  agentAddress: Hex;
  agentName: string;
  chainId: number;
  isTestnet: boolean;
}): Promise<{
  action: ApproveAgentAction;
  signatureHex: Hex;
}> {
  const { action } = createApproveAgentAction(params);
  const signatureHex = await params.signTypedDataAsync({
    domain: {
      name: USER_SIGNED_DOMAIN_NAME,
      version: "1",
      chainId: params.chainId,
      verifyingContract: ZERO_ADDRESS,
    },
    types: {
      EIP712Domain: EIP712_DOMAIN_TYPES,
      ...APPROVE_AGENT_TYPES,
    },
    primaryType: APPROVE_AGENT_PRIMARY_TYPE,
    message: {
      hyperliquidChain: action.hyperliquidChain,
      agentAddress: action.agentAddress,
      agentName: action.agentName,
      nonce: action.nonce,
    },
  });

  return {
    action,
    signatureHex,
  };
}

function getRawSignedTransaction(result: unknown): Hex | null {
  if (typeof result === "string" && result.startsWith("0x")) {
    return result as Hex;
  }
  if (!result || typeof result !== "object") return null;

  const record = result as Record<string, unknown>;
  const candidates = [
    record.raw,
    record.rawTransaction,
    record.signedTransaction,
    record.serializedTransaction,
    record.result,
  ];
  const raw = candidates.find(
    (value): value is string =>
      typeof value === "string" && value.startsWith("0x"),
  );
  return raw ? (raw as Hex) : null;
}

function decodeJsonBytes(bytes: Uint8Array): unknown {
  const text = new TextDecoder().decode(bytes);
  if (!text) return null;
  try {
    return JSON.parse(text);
  } catch {
    return text;
  }
}

function encodeJsonBytes(value: unknown): Uint8Array {
  return new TextEncoder().encode(JSON.stringify(value));
}

function withExpectedFrom(
  transaction: EvmTransactionRequest,
  address: Hex,
): EvmTransactionRequest {
  if (
    transaction.from &&
    transaction.from.toLowerCase() !== address.toLowerCase()
  ) {
    throw new Error(
      "[HypurrConnect] EVM transaction `from` does not match the connected wallet.",
    );
  }
  return transaction.from ? transaction : { ...transaction, from: address };
}

function currentReturnTo(): string {
  const url = new URL(window.location.href);
  for (const param of [
    "code",
    "error",
    "error_description",
    "token",
    "token_type",
    "token_source",
    "state",
    "scope",
  ]) {
    url.searchParams.delete(param);
  }
  return url.toString();
}

function cleanAuthCallbackUrl(): void {
  const cleanUrl = new URL(window.location.href);
  for (const param of [
    "code",
    "error",
    "error_description",
    "token",
    "token_type",
    "token_source",
    "state",
    "scope",
  ]) {
    cleanUrl.searchParams.delete(param);
  }
  window.history.replaceState({}, document.title, cleanUrl.toString());
}

function base64UrlEncode(bytes: Uint8Array): string {
  let value = "";
  for (const byte of bytes) {
    value += String.fromCharCode(byte);
  }
  return btoa(value).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
}

function randomState(): string {
  const bytes = new Uint8Array(16);
  crypto.getRandomValues(bytes);
  return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join(
    "",
  );
}

function randomCodeVerifier(): string {
  const bytes = new Uint8Array(64);
  crypto.getRandomValues(bytes);
  return base64UrlEncode(bytes);
}

async function createCodeChallenge(codeVerifier: string): Promise<string> {
  if (!crypto.subtle) {
    throw new Error(
      "[HypurrConnect] Web Crypto API is required for Telegram auth.",
    );
  }
  const digest = await crypto.subtle.digest(
    "SHA-256",
    new TextEncoder().encode(codeVerifier),
  );
  return base64UrlEncode(new Uint8Array(digest));
}

function normalizeScopes(scope?: string | string[]): string {
  if (Array.isArray(scope)) return scope.join(" ");
  return scope?.trim() || DEFAULT_TELEGRAM_SCOPES.join(" ");
}

function normalizeClientId(clientId: unknown): string {
  const normalized = typeof clientId === "string" ? clientId.trim() : "";
  if (!normalized) {
    throw new Error("[HypurrConnect] config.clientId is required.");
  }
  return normalized;
}

function codeVerifierStorageKey(state: string): string {
  return `${TELEGRAM_AUTH_CODE_VERIFIER_PREFIX}${state}`;
}

function returnToStorageKey(state: string): string {
  return `${TELEGRAM_AUTH_RETURN_TO_PREFIX}${state}`;
}

function storeTelegramAuthSession(
  state: string,
  codeVerifier: string,
  returnTo: string,
): void {
  const previousState = sessionStorage.getItem(TELEGRAM_AUTH_STATE_KEY);
  if (previousState) {
    sessionStorage.removeItem(codeVerifierStorageKey(previousState));
    sessionStorage.removeItem(returnToStorageKey(previousState));
  }
  sessionStorage.setItem(TELEGRAM_AUTH_STATE_KEY, state);
  sessionStorage.setItem(codeVerifierStorageKey(state), codeVerifier);
  sessionStorage.setItem(returnToStorageKey(state), returnTo);
}

function clearTelegramAuthSession(state: string): void {
  sessionStorage.removeItem(TELEGRAM_AUTH_STATE_KEY);
  sessionStorage.removeItem(codeVerifierStorageKey(state));
  sessionStorage.removeItem(returnToStorageKey(state));
}

function takeTelegramAuthSession(state: string): {
  codeVerifier: string | null;
  returnTo: string | null;
} | null {
  const expectedState = sessionStorage.getItem(TELEGRAM_AUTH_STATE_KEY);
  sessionStorage.removeItem(TELEGRAM_AUTH_STATE_KEY);
  if (!expectedState || state !== expectedState) {
    if (expectedState) {
      sessionStorage.removeItem(codeVerifierStorageKey(expectedState));
      sessionStorage.removeItem(returnToStorageKey(expectedState));
    }
    return null;
  }

  const codeVerifierKey = codeVerifierStorageKey(state);
  const returnToKey = returnToStorageKey(state);
  const codeVerifier = sessionStorage.getItem(codeVerifierKey);
  const returnTo = sessionStorage.getItem(returnToKey);
  sessionStorage.removeItem(codeVerifierKey);
  sessionStorage.removeItem(returnToKey);
  return { codeVerifier, returnTo };
}

function fallbackAuthTokenUrl(authHubUrl?: string): string {
  const url = new URL(authHubUrl || DEFAULT_AUTH_HUB_URL);
  url.pathname = "/oauth/token";
  url.search = "";
  url.hash = "";
  return url.toString();
}

function authMetadataUrls(authHubUrl?: string): string[] {
  const authUrl = new URL(authHubUrl || DEFAULT_AUTH_HUB_URL);
  const urls = [
    new URL("/.well-known/oauth-authorization-server", authUrl).toString(),
    new URL("/.well-known/openid-configuration", authUrl).toString(),
  ];

  const authPath = authUrl.pathname.replace(/\/+$/, "");
  const basePath = authPath.replace(/\/[^/]*$/, "");
  if (basePath) {
    urls.push(
      new URL(
        `/.well-known/oauth-authorization-server${basePath}`,
        authUrl,
      ).toString(),
    );
  }

  return Array.from(new Set(urls));
}

async function tokenUrlFromMetadata(
  authHubUrl?: string,
): Promise<string | undefined> {
  for (const metadataUrl of authMetadataUrls(authHubUrl)) {
    try {
      const response = await fetch(metadataUrl, {
        headers: { accept: "application/json" },
      });
      if (!response.ok) continue;
      const metadata = (await response.json()) as { token_endpoint?: unknown };
      const tokenEndpoint = metadata.token_endpoint;
      if (typeof tokenEndpoint === "string" && tokenEndpoint.trim()) {
        return tokenEndpoint.trim();
      }
    } catch {
      // Metadata discovery is best effort; fall back to the conventional route.
    }
  }

  return undefined;
}

async function resolveAuthTokenUrl(authHubUrl?: string): Promise<string> {
  return (
    (await tokenUrlFromMetadata(authHubUrl)) || fallbackAuthTokenUrl(authHubUrl)
  );
}

function getTokenFromExchangeResponse(data: unknown): string | null {
  if (typeof data === "string") {
    const token = data.trim();
    return token || null;
  }
  if (typeof data !== "object" || data === null) return null;

  const response = data as {
    access_token?: unknown;
    jwt?: unknown;
    token?: unknown;
  };
  for (const token of [response.token, response.access_token, response.jwt]) {
    if (typeof token === "string" && token.trim()) return token.trim();
  }
  return null;
}

async function exchangeTelegramAuthCode({
  authHubUrl,
  clientId,
  code,
  codeVerifier,
  returnTo,
}: {
  authHubUrl?: string;
  clientId: string;
  code: string;
  codeVerifier: string;
  returnTo: string;
}): Promise<string> {
  const body = new URLSearchParams({
    client_id: clientId,
    code,
    code_verifier: codeVerifier,
    grant_type: "authorization_code",
    return_to: returnTo,
  });

  const response = await fetch(await resolveAuthTokenUrl(authHubUrl), {
    method: "POST",
    headers: {
      accept: "application/json",
      "content-type": "application/x-www-form-urlencoded",
    },
    body,
  });
  const responseText = await response.text();

  if (!response.ok) {
    const detail = responseText.trim();
    throw new Error(
      detail
        ? `[HypurrConnect] Auth code exchange failed: ${detail}`
        : `[HypurrConnect] Auth code exchange failed with HTTP ${response.status}.`,
    );
  }

  let responseData: unknown = responseText;
  if (responseText) {
    try {
      responseData = JSON.parse(responseText) as unknown;
    } catch {
      responseData = responseText;
    }
  }

  const token = getTokenFromExchangeResponse(responseData);
  if (!token) {
    throw new Error("[HypurrConnect] Auth code exchange did not return a JWT.");
  }
  return token;
}

type TelegramAuthCallback = {
  type?: typeof TELEGRAM_AUTH_MESSAGE;
  state: string;
  token?: string;
  code?: string;
  error?: string;
};

function isTelegramAuthMessage(data: unknown): data is {
  type: typeof TELEGRAM_AUTH_MESSAGE;
  state: string;
  token?: string;
  code?: string;
  error?: string;
} {
  if (typeof data !== "object" || data === null) return false;
  if (!("type" in data) || !("state" in data)) return false;
  const message = data as {
    type: unknown;
    state: unknown;
    token?: unknown;
    code?: unknown;
    error?: unknown;
  };
  const hasToken = typeof message.token === "string";
  const hasCode = typeof message.code === "string";
  const hasError = typeof message.error === "string";
  return (
    message.type === TELEGRAM_AUTH_MESSAGE &&
    typeof message.state === "string" &&
    (hasToken || hasCode || hasError)
  );
}

const HypurrConnectContext = createContext<InternalConnectState | null>(null);

export function useHypurrConnect(): HypurrConnectState {
  const ctx = useContext(HypurrConnectContext);
  if (!ctx)
    throw new Error(
      "useHypurrConnect must be used within <HypurrConnectProvider>",
    );
  return ctx;
}

/** @internal — gives library components access to fields not on the public API */
export function useHypurrConnectInternal(): InternalConnectState {
  const ctx = useContext(HypurrConnectContext);
  if (!ctx)
    throw new Error(
      "useHypurrConnectInternal must be used within <HypurrConnectProvider>",
    );
  return ctx;
}

export function HypurrConnectProvider({
  config,
  children,
}: {
  config: HypurrConnectConfig;
  children: ReactNode;
}) {
  const tgClient = useMemo(() => createTelegramClient(config), [config]);
  const staticClient = useMemo(() => createStaticClient(config), [config]);

  // ── Telegram auth state ──────────────────────────────────────
  const [tgAuthToken, setTgAuthToken] = useState<string | null>(() => {
    try {
      return localStorage.getItem(TELEGRAM_STORAGE_KEY);
    } catch {
      return null;
    }
  });
  const [tgUser, setTgUser] = useState<HypurrTelegramUser | null>(null);
  const [tgLoading, setTgLoading] = useState(false);
  const [tgError, setTgError] = useState<string | null>(null);

  const authDataMap = useMemo(() => ({}), []);
  const telegramRpcOptions = useMemo<RpcOptions | undefined>(
    () =>
      tgAuthToken
        ? { meta: { authorization: `Bearer ${tgAuthToken}` } }
        : undefined,
    [tgAuthToken],
  );

  const [tgUserTick, setTgUserTick] = useState(0);

  // Auto-disconnect when the server rejects telegram auth data.
  const onInvalidAuthRef = useRef<(() => void) | null>(null);
  onInvalidAuthRef.current = () => {
    console.warn(
      "[HypurrConnect] Invalid Telegram auth token — disconnecting.",
    );
    setTgAuthToken(null);
    setTgUser(null);
    setTgError(null);
    localStorage.removeItem(TELEGRAM_STORAGE_KEY);
    localStorage.removeItem(LEGACY_TELEGRAM_STORAGE_KEY);
  };

  const acceptTelegramToken = useCallback((token: string) => {
    setTgAuthToken(token);
    setTgError(null);
    localStorage.setItem(TELEGRAM_STORAGE_KEY, token);
    localStorage.removeItem(LEGACY_TELEGRAM_STORAGE_KEY);
  }, []);

  const handleTelegramAuthCallback = useCallback(
    (callback: TelegramAuthCallback) => {
      const authSession = takeTelegramAuthSession(callback.state);
      if (!authSession) {
        setTgError("Invalid auth callback state.");
        return;
      }

      if (callback.error) {
        setTgError(callback.error);
        return;
      }

      if (callback.code) {
        if (!authSession.codeVerifier) {
          setTgError("Missing auth code verifier.");
          return;
        }

        setTgLoading(true);
        setTgError(null);
        void exchangeTelegramAuthCode({
          authHubUrl: config.telegram?.authHubUrl,
          clientId: normalizeClientId(config.clientId),
          code: callback.code,
          codeVerifier: authSession.codeVerifier,
          returnTo: authSession.returnTo || currentReturnTo(),
        })
          .then(acceptTelegramToken)
          .catch((err) =>
            setTgError(err instanceof Error ? err.message : String(err)),
          )
          .finally(() => setTgLoading(false));
        return;
      }

      if (callback.token) {
        acceptTelegramToken(callback.token);
        return;
      }

      setTgError("Invalid auth callback.");
    },
    [
      acceptTelegramToken,
      config.clientId,
      config.telegram?.authHubUrl,
    ],
  );

  // Eagerly fetch the modal fonts on mount. They're declared via @import in the
  // injected stylesheet but browsers only fetch a webfont when visible text
  // first uses it — that lazy fetch makes scores/labels flash the system
  // fallback and render inconsistently across views. Loading them up front
  // keeps the bold mono numbers consistent.
  useEffect(() => {
    if (typeof document === "undefined" || !document.fonts) return;
    for (const face of [
      "500 1em 'Google Sans Code'",
      "700 1em 'Google Sans Code'",
      "600 1em Inter",
      "700 1em Inter",
    ]) {
      void document.fonts.load(face).catch(() => {});
    }
  }, []);

  useEffect(() => {
    const params = new URLSearchParams(window.location.search);
    const token = params.get("token");
    const code = params.get("code");
    const error =
      params.get("error_description") || params.get("error") || undefined;
    if (!token && !code && !error) {
      localStorage.removeItem(LEGACY_TELEGRAM_STORAGE_KEY);
      return;
    }

    const callbackState = params.get("state") ?? "";
    const callback = {
      code: code || undefined,
      error,
      state: callbackState,
      token: token || undefined,
      type: TELEGRAM_AUTH_MESSAGE,
    } satisfies TelegramAuthCallback;

    if (window.opener && window.opener !== window) {
      window.opener.postMessage(callback, window.location.origin);
      window.close();
      return;
    }

    cleanAuthCallbackUrl();
    handleTelegramAuthCallback(callback);
  }, [handleTelegramAuthCallback]);

  useEffect(() => {
    function onMessage(event: MessageEvent) {
      if (event.origin !== window.location.origin) return;
      if (!isTelegramAuthMessage(event.data)) return;

      handleTelegramAuthCallback(event.data);
    }

    window.addEventListener("message", onMessage);
    return () => window.removeEventListener("message", onMessage);
  }, [handleTelegramAuthCallback]);

  useEffect(() => {
    if (!tgAuthToken || !telegramRpcOptions) return;
    let cancelled = false;
    setTgLoading(true);
    setTgError(null);

    (async () => {
      try {
        const [{ response: userResp }, { response: walletsResp }] =
          await Promise.all([
            tgClient.telegramUser({ authData: {} }, telegramRpcOptions),
            tgClient.telegramUserWallets({ authData: {} }, telegramRpcOptions),
          ]);
        if (cancelled) return;
        const user = (userResp as TelegramUserResponse).user ?? null;
        if (user) {
          // TelegramUser.wallets lacks twap/scale sessions; replace with
          // the full wallets from TelegramUserWallets which populates them.
          user.wallets = walletsResp.wallets;
        }
        setTgUser(user);
      } catch (err) {
        if (cancelled) return;
        if (isInvalidTelegramAuthError(err)) {
          onInvalidAuthRef.current?.();
          return;
        }
        console.error("[HypurrConnect] gRPC TelegramUser failed:", err);
        setTgError(err instanceof Error ? err.message : String(err));
      } finally {
        if (!cancelled) setTgLoading(false);
      }
    })();

    return () => {
      cancelled = true;
    };
  }, [tgAuthToken, telegramRpcOptions, tgClient, tgUserTick]);

  // ── EOA auth state ───────────────────────────────────────────
  const [eoaAddress, setEoaAddress] = useState<`0x${string}` | null>(null);
  const [agent, setAgent] = useState<StoredAgent | null>(null);
  const [eoaLoading, setEoaLoading] = useState(false);
  const [eoaError, setEoaError] = useState<string | null>(null);
  const eoaSignerRef = useRef<EoaSigner | null>(null);

  // ── Derived auth ─────────────────────────────────────────────
  const authMethod: AuthMethod = tgAuthToken
    ? "telegram"
    : eoaAddress
      ? "eoa"
      : null;

  // ── Multi-wallet state (Telegram) ─────────────────────────────
  const [wallets, setWallets] = useState<HyperliquidWallet[]>([]);
  const [selectedWalletId, setSelectedWalletId] = useState<number>(0);
  const [packs, setPacks] = useState<TelegramChatWalletPack[]>([]);

  const refreshWallets = useCallback(() => setTgUserTick((t) => t + 1), []);

  useEffect(() => {
    if (authMethod !== "telegram" || !tgUser) {
      setWallets([]);
      setSelectedWalletId(0);
      setPacks([]);
      return;
    }

    const userWallets = tgUser.wallets ?? [];
    setWallets(userWallets);
    setPacks(tgUser.packs ?? []);

    const defaultId = tgUser.walletId || userWallets[0]?.id || 0;
    setSelectedWalletId((prev) => {
      if (prev && userWallets.some((w) => w.id === prev)) return prev;
      return defaultId;
    });
  }, [authMethod, tgUser]);

  const selectedWallet = useMemo(
    () => wallets.find((w) => w.id === selectedWalletId) ?? wallets[0] ?? null,
    [wallets, selectedWalletId],
  );

  const selectWallet = useCallback(
    (walletId: number) => {
      if (wallets.some((w) => w.id === walletId)) {
        setSelectedWalletId(walletId);
      }
    },
    [wallets],
  );

  // ── Session poller (TWAP + Scale for selected wallet) ──────
  const pollInterval = config.sessionPollInterval ?? 5_000;

  useEffect(() => {
    if (authMethod !== "telegram" || !selectedWalletId || !pollInterval) return;

    let cancelled = false;

    const poll = async () => {
      try {
        const [{ response: twapResp }, { response: scaleResp }] =
          await Promise.all([
            tgClient.hyperliquidWalletTwapSessions(
              {
                authData: {},
                walletId: selectedWalletId,
              },
              telegramRpcOptions,
            ),
            tgClient.hyperliquidWalletScaleSessions(
              {
                authData: {},
                walletId: selectedWalletId,
              },
              telegramRpcOptions,
            ),
          ]);
        if (cancelled) return;
        setWallets((prev) =>
          prev.map((w) =>
            w.id === selectedWalletId
              ? {
                  ...w,
                  twapSessions: twapResp.sessions,
                  scaleSessions: scaleResp.sessions,
                }
              : w,
          ),
        );
      } catch (err) {
        if (isInvalidTelegramAuthError(err)) {
          onInvalidAuthRef.current?.();
          return;
        }
        // Silently ignore poll errors — next tick will retry.
      }
    };

    // Fire immediately on mount / wallet switch, then on interval.
    poll();
    const id = setInterval(poll, pollInterval);
    return () => {
      cancelled = true;
      clearInterval(id);
    };
  }, [
    authMethod,
    selectedWalletId,
    pollInterval,
    tgClient,
    telegramRpcOptions,
  ]);

  const user = useMemo<HypurrUser | null>(() => {
    if (tgAuthToken && authMethod === "telegram" && selectedWallet && tgUser) {
      const mediaUrl = normalizeMediaUrl(config.mediaUrl);
      return {
        address: selectedWallet.ethereumAddress,
        walletId: selectedWallet.id,
        displayName: tgUser.telegramUsername
          ? `@${tgUser.telegramUsername}`
          : `Telegram ${tgUser.telegramId}`,
        photoUrl: (() => {
          // `pictureFileId` is read at runtime; the schema may not type it.
          const fileId = (tgUser as unknown as { pictureFileId?: string })
            .pictureFileId;
          return fileId ? `${mediaUrl}/${fileId}` : undefined;
        })(),
        authMethod: "telegram",
        telegramId: String(tgUser.telegramId),
        hfunScore: tgUser?.reputation?.hfunScore,
        reputationScore: tgUser?.reputation?.reputationScore,
        addressBook: tgUser.addressBook,
      };
    }
    if (eoaAddress && authMethod === "eoa") {
      return {
        address: eoaAddress,
        walletId: 0,
        displayName: `${eoaAddress.slice(0, 6)}...${eoaAddress.slice(-4)}`,
        authMethod: "eoa",
      };
    }
    return null;
  }, [
    tgAuthToken,
    selectedWallet,
    eoaAddress,
    authMethod,
    tgUser,
    config.mediaUrl,
  ]);

  // ── Exchange client ──────────────────────────────────────────
  // Telegram: dummy local wallet → GrpcExchangeTransport → HyperliquidCoreAction (server signs)
  // EOA: dual wallet — agent key for L1 actions, master signer for user-signed
  //   actions (transfers, withdrawals, etc.). The dual wallet inspects the
  //   EIP-712 domain name to decide which key signs each request.
  //   When a signer is available but no agent exists yet, the dual wallet
  //   auto-provisions an agent on the first L1 action (triggers one extra
  //   wallet popup for the approveAgent user-signed action).

  const onDeadAgentRef = useRef<((address: `0x${string}`) => void) | null>(
    null,
  );
  onDeadAgentRef.current = (addr: `0x${string}`) => {
    clearStoredAgent(addr);
    setAgent(null);
    setEoaError("Agent expired or was deregistered. Please reconnect.");
  };

  // Mutable slot for the agent signer — the dual wallet reads this so it can
  // pick up a newly provisioned agent without waiting for a React re-render.
  const agentSignerRef = useRef<PrivateKeySigner | null>(
    agent ? new PrivateKeySigner(agent.privateKey) : null,
  );
  useEffect(() => {
    agentSignerRef.current = agent
      ? new PrivateKeySigner(agent.privateKey)
      : null;
  }, [agent]);

  // Lock to prevent concurrent auto-provisioning attempts
  const provisioningRef = useRef<Promise<PrivateKeySigner> | null>(null);

  const agentReady =
    authMethod === "telegram" || (authMethod === "eoa" && !!agent);

  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  const exchange = useMemo<ExchangeClient<any> | null>(() => {
    if (authMethod === "telegram" && user?.address) {
      const transport = new GrpcExchangeTransport({
        isTestnet: config.isTestnet ?? false,
        telegramClient: tgClient,
        rpcOptions: telegramRpcOptions,
        walletId: user.walletId,
        onAuthError: () => onInvalidAuthRef.current?.(),
      });
      return new ExchangeClient({
        transport,
        wallet: createExternalSigningWallet(user.address as Hex),
      });
    }

    if (authMethod === "eoa" && eoaAddress) {
      const hasSigner = !!eoaSignerRef.current;

      if (!agent && !hasSigner) {
        const noAgentTransport: IRequestTransport = {
          isTestnet: config.isTestnet ?? false,
          request(): Promise<never> {
            throw new Error(
              "[HypurrConnect] No agent key approved and no wallet signer available. " +
                "Either call approveAgent(signTypedDataAsync) or pass a signer to " +
                "connectEoa(address, { signTypedData, chainId }).",
            );
          },
        };
        return new ExchangeClient({
          transport: noAgentTransport,
          wallet: createExternalSigningWallet(eoaAddress),
        });
      }

      const isTestnet = config.isTestnet ?? false;
      const inner = new HttpTransport({ isTestnet });
      const deadAgentAddr = eoaAddress;
      const guardedTransport: IRequestTransport = {
        isTestnet: inner.isTestnet,
        async request<T>(
          endpoint: "info" | "exchange" | "explorer",
          payload: unknown,
          signal?: AbortSignal,
        ): Promise<T> {
          try {
            return await inner.request<T>(endpoint, payload, signal);
          } catch (err) {
            if (endpoint === "exchange" && isDeadAgentError(err)) {
              onDeadAgentRef.current?.(deadAgentAddr);
            }
            throw err;
          }
        },
      };

      const signerRef = eoaSignerRef;
      const agentRef = agentSignerRef;
      const provRef = provisioningRef;
      const ownerAddress = eoaAddress;

      /**
       * Auto-provision an agent key when one doesn't exist yet.
       *
       * Bypasses the SDK's `executeUserSignedAction` (and its per-address
       * semaphore) to avoid deadlocking when called from inside
       * `dualWallet.signTypedData`, which is already inside the SDK's
       * `executeL1Action` lock for the same address.
       */
      const ensureAgent = async (): Promise<PrivateKeySigner> => {
        const existing = agentRef.current;
        if (existing) return existing;

        if (provRef.current) return provRef.current;

        const signer = signerRef.current;
        if (!signer) {
          throw new Error(
            "[HypurrConnect] No wallet signer available to auto-provision agent. " +
              "Pass a signer to connectEoa(address, { signTypedData, chainId }).",
          );
        }

        provRef.current = (async () => {
          try {
            const { privateKey, address: agentAddress } =
              await generateAgentKey();

            const chainIdHex =
              `0x${signer.chainId.toString(16)}` as `0x${string}`;
            const nonce = Date.now();
            const action = {
              type: "approveAgent" as const,
              signatureChainId: chainIdHex,
              hyperliquidChain: (isTestnet ? "Testnet" : "Mainnet") as
                | "Testnet"
                | "Mainnet",
              agentAddress: agentAddress.toLowerCase() as `0x${string}`,
              agentName: AGENT_NAME,
              nonce,
            };

            const approveAgentTypes = {
              "HyperliquidTransaction:ApproveAgent": [
                { name: "hyperliquidChain", type: "string" },
                { name: "agentAddress", type: "address" },
                { name: "agentName", type: "string" },
                { name: "nonce", type: "uint64" },
              ],
            };

            const wallet = {
              // eslint-disable-next-line @typescript-eslint/no-explicit-any
              signTypedData(params: any) {
                return signer.signTypedData(params);
              },
              getAddresses: async () => [ownerAddress] as `0x${string}`[],
              getChainId: async () => signer.chainId,
            };

            const signature = await signUserSignedAction({
              wallet,
              action,
              types: approveAgentTypes,
            });

            const apiUrl = isTestnet
              ? "https://api-ui.hyperliquid-testnet.xyz/exchange"
              : "https://api.hyperliquid.xyz/exchange";

            const res = await fetch(apiUrl, {
              method: "POST",
              headers: { "Content-Type": "application/json" },
              body: JSON.stringify({ action, signature, nonce }),
            });

            const body = await res.json();
            if (body?.status === "err") {
              throw new Error(
                `approveAgent API error: ${body.response ?? JSON.stringify(body)}`,
              );
            }

            const remote = await fetchActiveAgent(ownerAddress, isTestnet);
            const validUntil =
              remote?.validUntil ?? Date.now() + 7 * 24 * 60 * 60 * 1000;

            const stored: StoredAgent = {
              privateKey,
              address: agentAddress,
              approvedAt: Date.now(),
              validUntil,
            };
            saveAgent(ownerAddress, stored);

            const newSigner = new PrivateKeySigner(privateKey);
            agentRef.current = newSigner;
            setAgent(stored);

            return newSigner;
          } finally {
            provRef.current = null;
          }
        })();

        return provRef.current;
      };

      // Dual wallet: routes signing based on the EIP-712 domain.
      // "Exchange" domain → L1 action → agent key signs (auto-provisions if needed).
      // "HyperliquidSignTransaction" domain → user-signed → master wallet (popup).
      const dualWallet: AbstractViemLocalAccount = {
        address: ownerAddress,
        async signTypedData(params): Promise<`0x${string}`> {
          if (params.domain.name === "HyperliquidSignTransaction") {
            const signer = signerRef.current;
            if (!signer) {
              throw new Error(
                "[HypurrConnect] No wallet signer available for user-signed actions. " +
                  "Pass a signer to connectEoa(address, { signTypedData, chainId }).",
              );
            }
            return signer.signTypedData(
              params as Parameters<typeof signer.signTypedData>[0],
            );
          }

          const agentSigner = await ensureAgent();
          return agentSigner.signTypedData(params);
        },
      };

      return new ExchangeClient({
        transport: guardedTransport,
        wallet: dualWallet,
        signatureChainId: () => {
          const id = signerRef.current?.chainId ?? 42161;
          return `0x${id.toString(16)}` as `0x${string}`;
        },
      });
    }

    return null;
  }, [
    authMethod,
    user,
    agent,
    eoaAddress,
    config.isTestnet,
    tgClient,
    telegramRpcOptions,
  ]);

  const handleClearAgent = useCallback(() => {
    if (eoaAddress) {
      clearStoredAgent(eoaAddress);
      setAgent(null);
    }
  }, [eoaAddress]);

  const signEvmTransaction = useCallback(
    async (transaction: EvmTransactionRequest): Promise<Hex> => {
      if (authMethod === "eoa") {
        if (!eoaAddress) {
          throw new Error("[HypurrConnect] No EOA wallet connected.");
        }

        const signer = eoaSignerRef.current;
        if (!signer) {
          throw new Error(
            "[HypurrConnect] No EOA signer available. Pass a signer to connectEoa(address, signer).",
          );
        }

        const tx = withExpectedFrom(transaction, eoaAddress);
        const result = signer.signTransaction
          ? await signer.signTransaction(tx)
          : signer.request
            ? await signer.request({
                method: "eth_signTransaction",
                params: [tx],
              })
            : null;
        const rawTransaction = getRawSignedTransaction(result);
        if (!rawTransaction) {
          throw new Error(
            "[HypurrConnect] EOA signer did not return a raw transaction.",
          );
        }
        return rawTransaction;
      }

      if (authMethod === "telegram") {
        if (!telegramRpcOptions) {
          throw new Error("[HypurrConnect] No Telegram RPC session available.");
        }
        if (!selectedWallet) {
          throw new Error("[HypurrConnect] No Telegram wallet selected.");
        }
        if (
          selectedWallet.id <= 0 ||
          selectedWallet.isReadOnly ||
          selectedWallet.isAgent
        ) {
          throw new Error(
            "[HypurrConnect] Select a Telegram private-key wallet to sign EVM transactions.",
          );
        }
        if (!isAddress(selectedWallet.ethereumAddress)) {
          throw new Error(
            "[HypurrConnect] Selected Telegram wallet does not have a valid EVM address.",
          );
        }

        const tx = withExpectedFrom(
          transaction,
          selectedWallet.ethereumAddress,
        );
        try {
          const signResponse = await tgClient.eVMSignTransaction(
            {
              authData: {},
              walletId: selectedWallet.id,
              params: encodeJsonBytes(tx),
            },
            telegramRpcOptions,
          );
          const rawTransaction = getRawSignedTransaction(
            decodeJsonBytes(signResponse.response.result),
          );
          if (!rawTransaction) {
            throw new Error(
              "[HypurrConnect] Telegram signer did not return a raw transaction.",
            );
          }
          return rawTransaction;
        } catch (err) {
          if (isInvalidTelegramAuthError(err)) {
            onInvalidAuthRef.current?.();
          }
          throw err;
        }
      }

      throw new Error("[HypurrConnect] No wallet connected.");
    },
    [authMethod, eoaAddress, selectedWallet, telegramRpcOptions, tgClient],
  );

  // ── Wallet management (Telegram only) ───────────────────────
  const createWallet = useCallback(
    async (name: string): Promise<HyperliquidWallet> => {
      const { response } = await tgClient.hyperliquidWalletCreate(
        {
          authData: {},
          name,
        },
        telegramRpcOptions,
      );
      refreshWallets();
      if (!response.wallet)
        throw new Error("Wallet creation returned no wallet");
      return response.wallet;
    },
    [tgClient, telegramRpcOptions, refreshWallets],
  );

  const deleteWallet = useCallback(
    async (walletId: number): Promise<void> => {
      await tgClient.hyperliquidWalletDelete(
        {
          authData: {},
          walletId,
        },
        telegramRpcOptions,
      );
      if (walletId === selectedWalletId) {
        const remaining = wallets.filter((w) => w.id !== walletId);
        setSelectedWalletId(remaining[0]?.id ?? 0);
      }
      refreshWallets();
    },
    [tgClient, telegramRpcOptions, selectedWalletId, wallets, refreshWallets],
  );

  const renameWallet = useCallback(
    async (walletId: number, name: string): Promise<void> => {
      await tgClient.hyperliquidWalletUpdate(
        {
          authData: {},
          walletId,
          name,
        },
        telegramRpcOptions,
      );
      refreshWallets();
    },
    [tgClient, telegramRpcOptions, refreshWallets],
  );

  const renewAgentWallet = useCallback(
    async ({
      walletId,
      ownerAddress,
      signTypedDataAsync,
      chainId,
      approvalDurationMs,
      agentName,
    }: RenewAgentWalletParams): Promise<void> => {
      if (authMethod !== "telegram") {
        throw new Error(
          "[HypurrConnect] Agent wallet renewal is only available for Telegram wallets.",
        );
      }
      if (!isAddress(ownerAddress)) {
        throw new Error(
          "[HypurrConnect] Connect the owner EOA wallet before renewing this agent.",
        );
      }
      if (!telegramRpcOptions) {
        throw new Error("[HypurrConnect] No Telegram RPC session available.");
      }

      const wallet = wallets.find((w) => w.id === walletId);
      if (!wallet) {
        throw new Error("[HypurrConnect] Agent wallet not found.");
      }
      if (!wallet.isAgent) {
        throw new Error(
          "[HypurrConnect] Selected wallet is not an agent wallet.",
        );
      }
      if (!isAddress(wallet.ethereumAddress)) {
        throw new Error(
          "[HypurrConnect] Agent wallet does not have a valid owner EVM address.",
        );
      }
      if (ownerAddress.toLowerCase() !== wallet.ethereumAddress.toLowerCase()) {
        throw new Error(
          "[HypurrConnect] Connect the owner EOA for this agent wallet before renewing.",
        );
      }

      const agentAddress = wallet.agentEthereumAddress?.value;
      if (!isAddress(agentAddress)) {
        throw new Error(
          "[HypurrConnect] Agent wallet does not have a valid agent EVM address.",
        );
      }

      const isTestnet = config.isTestnet ?? false;
      const approvalAgentName =
        agentName ?? createTelegramAgentApprovalName(approvalDurationMs);
      setTgError(null);

      try {
        const { action, signatureHex } = await signApproveAgentAction({
          signTypedDataAsync,
          agentAddress,
          agentName: approvalAgentName,
          chainId,
          isTestnet,
        });

        await tgClient.hyperliquidAgentWalletRenew(
          {
            authData: {},
            address: { value: wallet.ethereumAddress },
            signature: {
              agentAddress: action.agentAddress,
              agentName: action.agentName,
              nonce: action.nonce,
              chainId,
              signature: signatureHex,
            },
          },
          telegramRpcOptions,
        );

        const remote = await fetchAgentByAddress(
          ownerAddress,
          agentAddress,
          isTestnet,
        );
        if (remote && remote.validUntil <= Date.now()) {
          throw new Error(
            "[HypurrConnect] Agent renewal was submitted, but the agent is still expired.",
          );
        }

        refreshWallets();
      } catch (err) {
        console.error("[HypurrConnect] Agent wallet renewal failed:", err);
        setTgError(err instanceof Error ? err.message : String(err));
        throw err;
      }
    },
    [
      authMethod,
      config.isTestnet,
      refreshWallets,
      telegramRpcOptions,
      tgClient,
      wallets,
    ],
  );

  const createWalletPack = useCallback(
    async (name: string): Promise<number> => {
      const { response } = await tgClient.telegramChatWalletPackCreate(
        {
          authData: {},
          name,
        },
        telegramRpcOptions,
      );
      refreshWallets();
      return response.packId;
    },
    [tgClient, telegramRpcOptions, refreshWallets],
  );

  const addPackLabel = useCallback(
    async (params: {
      walletAddress: string;
      walletLabel: string;
      packId: number;
    }): Promise<void> => {
      await tgClient.telegramChatWalletPackLabelAdd(
        {
          authData: {},
          ...params,
        },
        telegramRpcOptions,
      );
      refreshWallets();
    },
    [tgClient, telegramRpcOptions, refreshWallets],
  );

  const modifyPackLabel = useCallback(
    async (params: {
      walletLabelOld: string;
      walletLabelNew: string;
      packId: number;
    }): Promise<void> => {
      await tgClient.telegramChatWalletPackLabelModify(
        {
          authData: {},
          ...params,
        },
        telegramRpcOptions,
      );
      refreshWallets();
    },
    [tgClient, telegramRpcOptions, refreshWallets],
  );

  const removePackLabel = useCallback(
    async (params: { walletLabel: string; packId: number }): Promise<void> => {
      await tgClient.telegramChatWalletPackLabelRemove(
        {
          authData: {},
          ...params,
        },
        telegramRpcOptions,
      );
      refreshWallets();
    },
    [tgClient, telegramRpcOptions, refreshWallets],
  );

  // ── TWAP session management (Telegram only) ─────────────────
  const createTwap = useCallback(
    async (
      params: Omit<
        Parameters<typeof tgClient.hyperliquidTwapCreate>[0],
        "authData" | "walletId"
      >,
    ) => {
      const { response } = await tgClient.hyperliquidTwapCreate(
        {
          authData: {},
          walletId: selectedWalletId,
          ...params,
        },
        telegramRpcOptions,
      );
      if (!response.session)
        throw new Error("TWAP creation returned no session");
      const session = response.session;
      setWallets((prev) =>
        prev.map((w) =>
          w.id === selectedWalletId
            ? { ...w, twapSessions: [...w.twapSessions, session] }
            : w,
        ),
      );
      return session;
    },
    [tgClient, telegramRpcOptions, selectedWalletId],
  );

  const modifyTwap = useCallback(
    async (
      params: Omit<
        Parameters<typeof tgClient.hyperliquidTwapModify>[0],
        "authData" | "walletId"
      >,
    ) => {
      const { response } = await tgClient.hyperliquidTwapModify(
        {
          authData: {},
          walletId: selectedWalletId,
          ...params,
        },
        telegramRpcOptions,
      );
      if (!response.session) throw new Error("TWAP modify returned no session");
      const session = response.session;
      setWallets((prev) =>
        prev.map((w) =>
          w.id === selectedWalletId
            ? {
                ...w,
                twapSessions: w.twapSessions.map((s) =>
                  s.id === session.id ? session : s,
                ),
              }
            : w,
        ),
      );
      return session;
    },
    [tgClient, telegramRpcOptions, selectedWalletId],
  );

  const cancelTwap = useCallback(
    async (sessionId: number) => {
      await tgClient.hyperliquidTwapCancel(
        {
          authData: {},
          walletId: selectedWalletId,
          twapSessionId: sessionId,
        },
        telegramRpcOptions,
      );
      setWallets((prev) =>
        prev.map((w) =>
          w.id === selectedWalletId
            ? {
                ...w,
                twapSessions: w.twapSessions.filter((s) => s.id !== sessionId),
              }
            : w,
        ),
      );
    },
    [tgClient, telegramRpcOptions, selectedWalletId],
  );

  // ── Scale session management (Telegram only) ───────────────
  const createScale = useCallback(
    async (
      params: Omit<
        Parameters<typeof tgClient.hyperliquidScaleCreate>[0],
        "authData" | "walletId"
      >,
    ) => {
      const { response } = await tgClient.hyperliquidScaleCreate(
        {
          authData: {},
          walletId: selectedWalletId,
          ...params,
        },
        telegramRpcOptions,
      );
      if (!response.session)
        throw new Error("Scale creation returned no session");
      const session = response.session;
      setWallets((prev) =>
        prev.map((w) =>
          w.id === selectedWalletId
            ? { ...w, scaleSessions: [...w.scaleSessions, session] }
            : w,
        ),
      );
      return session;
    },
    [tgClient, telegramRpcOptions, selectedWalletId],
  );

  const cancelScale = useCallback(
    async (sessionId: number) => {
      await tgClient.hyperliquidScaleCancel(
        {
          authData: {},
          walletId: selectedWalletId,
          scaleSessionId: sessionId,
        },
        telegramRpcOptions,
      );
      setWallets((prev) =>
        prev.map((w) =>
          w.id === selectedWalletId
            ? {
                ...w,
                scaleSessions: w.scaleSessions.filter(
                  (s) => s.id !== sessionId,
                ),
              }
            : w,
        ),
      );
    },
    [tgClient, telegramRpcOptions, selectedWalletId],
  );

  // ── Login modal state ────────────────────────────────────────
  const [loginModalOpen, setLoginModalOpen] = useState(false);
  const openLoginModal = useCallback(() => setLoginModalOpen(true), []);
  const closeLoginModal = useCallback(() => setLoginModalOpen(false), []);

  // ── Auth actions ─────────────────────────────────────────────
  const loginTelegram = useCallback(() => {
    const state = randomState();
    const codeVerifier = randomCodeVerifier();

    const configuredReturnTo = config.telegram?.returnTo;
    const returnTo =
      typeof configuredReturnTo === "function"
        ? configuredReturnTo()
        : configuredReturnTo || currentReturnTo();
    storeTelegramAuthSession(state, codeVerifier, returnTo);

    const width = 520;
    const height = 720;
    const left = window.screenX + Math.max(0, (window.outerWidth - width) / 2);
    const top = window.screenY + Math.max(0, (window.outerHeight - height) / 2);
    const popup = window.open(
      "about:blank",
      "hypurr_telegram_auth",
      [
        `width=${width}`,
        `height=${height}`,
        `left=${Math.round(left)}`,
        `top=${Math.round(top)}`,
        "resizable=yes",
        "scrollbars=yes",
      ].join(","),
    );

    void (async () => {
      try {
        const authUrl = new URL(
          config.telegram?.authHubUrl || DEFAULT_AUTH_HUB_URL,
        );
        authUrl.searchParams.set(
          "client_id",
          normalizeClientId(config.clientId),
        );
        authUrl.searchParams.set("return_to", returnTo);
        authUrl.searchParams.set("state", state);
        authUrl.searchParams.set(
          "scope",
          normalizeScopes(config.telegram?.scope),
        );
        authUrl.searchParams.set(
          "code_challenge",
          await createCodeChallenge(codeVerifier),
        );
        authUrl.searchParams.set("code_challenge_method", "S256");

        if (popup) {
          popup.location.assign(authUrl.toString());
          popup.focus();
          return;
        }

        window.location.assign(authUrl.toString());
      } catch (err) {
        clearTelegramAuthSession(state);
        if (popup && !popup.closed) popup.close();
        setTgError(err instanceof Error ? err.message : String(err));
      }
    })();
  }, [
    config.clientId,
    config.telegram?.authHubUrl,
    config.telegram?.returnTo,
    config.telegram?.scope,
  ]);

  const connectEoa = useCallback(
    (address: `0x${string}`, signer?: EoaSigner) => {
      eoaSignerRef.current = signer ?? null;
      setEoaAddress(address);
      setTgAuthToken(null);
      setTgUser(null);
      setTgError(null);
      setEoaError(null);
      localStorage.removeItem(TELEGRAM_STORAGE_KEY);
      localStorage.removeItem(LEGACY_TELEGRAM_STORAGE_KEY);

      const existing = loadAgent(address);
      if (existing && existing.validUntil > Date.now()) {
        setAgent(existing);
      } else {
        if (existing) clearStoredAgent(address);
        setAgent(null);
      }
    },
    [],
  );

  const approveAgentFn = useCallback(
    async (signTypedDataAsync: SignTypedDataFn, chainId: number) => {
      if (!eoaAddress) {
        throw new Error(
          "[HypurrConnect] Cannot approve agent: no EOA wallet connected. Call connectEoa(address) first.",
        );
      }

      const currentSigner = eoaSignerRef.current;
      eoaSignerRef.current = {
        signTypedData: signTypedDataAsync,
        signTransaction: currentSigner?.signTransaction,
        request: currentSigner?.request,
        chainId,
      };

      setEoaLoading(true);
      setEoaError(null);
      try {
        const existing = loadAgent(eoaAddress);
        if (existing) {
          const isTestnet = config.isTestnet ?? false;
          const valid = await isAgentValid(existing, eoaAddress, isTestnet);
          if (valid) {
            setAgent(existing);
            return;
          }
          clearStoredAgent(eoaAddress);
        }

        const { privateKey, address: agentAddress } = await generateAgentKey();
        const isTestnet = config.isTestnet ?? false;

        const chainIdHex = `0x${chainId.toString(16)}` as `0x${string}`;
        const nonce = Date.now();
        const action = {
          type: "approveAgent" as const,
          signatureChainId: chainIdHex,
          hyperliquidChain: (isTestnet ? "Testnet" : "Mainnet") as
            | "Testnet"
            | "Mainnet",
          agentAddress: agentAddress.toLowerCase() as `0x${string}`,
          agentName: AGENT_NAME,
          nonce,
        };

        const approveAgentTypes = {
          "HyperliquidTransaction:ApproveAgent": [
            { name: "hyperliquidChain", type: "string" },
            { name: "agentAddress", type: "address" },
            { name: "agentName", type: "string" },
            { name: "nonce", type: "uint64" },
          ],
        };

        const wallet = {
          // eslint-disable-next-line @typescript-eslint/no-explicit-any
          signTypedData(params: any) {
            return signTypedDataAsync(params);
          },
          getAddresses: async () => [eoaAddress] as `0x${string}`[],
          getChainId: async () => chainId,
        };

        const signature = await signUserSignedAction({
          wallet,
          action,
          types: approveAgentTypes,
        });

        const apiUrl = isTestnet
          ? "https://api.hyperliquid-testnet.xyz/exchange"
          : "https://api.hyperliquid.xyz/exchange";

        const res = await fetch(apiUrl, {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({ action, signature, nonce }),
        });

        const body = await res.json();
        if (body?.status === "err") {
          throw new Error(
            `approveAgent API error: ${body.response ?? JSON.stringify(body)}`,
          );
        }

        const remote = await fetchActiveAgent(eoaAddress, isTestnet);
        const validUntil =
          remote?.validUntil ?? Date.now() + 7 * 24 * 60 * 60 * 1000;

        const stored: StoredAgent = {
          privateKey,
          address: agentAddress,
          approvedAt: Date.now(),
          validUntil,
        };
        saveAgent(eoaAddress, stored);
        setAgent(stored);
      } catch (err) {
        console.error("[HypurrConnect] EOA agent approval failed:", err);
        setEoaError(err instanceof Error ? err.message : String(err));
        setAgent(null);
      } finally {
        setEoaLoading(false);
      }
    },
    [eoaAddress, config.isTestnet],
  );

  const logout = useCallback(() => {
    setTgUser(null);
    setTgError(null);
    setTgAuthToken(null);
    setEoaAddress(null);
    setAgent(null);
    setEoaError(null);
    eoaSignerRef.current = null;
    localStorage.removeItem(TELEGRAM_STORAGE_KEY);
    localStorage.removeItem(LEGACY_TELEGRAM_STORAGE_KEY);
  }, []);

  // ── Context value ────────────────────────────────────────────
  const value = useMemo<InternalConnectState>(
    () => ({
      user,
      isLoggedIn: !!user,
      isLoading: tgLoading || eoaLoading,
      error: tgError ?? eoaError,
      authMethod,
      exchange,

      wallets,
      selectedWalletId,
      selectWallet,

      createWallet,
      deleteWallet,
      renameWallet,
      renewAgentWallet,
      refreshWallets,

      packs,
      createWalletPack,
      addPackLabel,
      modifyPackLabel,
      removePackLabel,

      createTwap,
      modifyTwap,
      cancelTwap,

      createScale,
      cancelScale,

      loginModalOpen,
      openLoginModal,
      closeLoginModal,

      loginTelegram,
      connectEoa,
      signEvmTransaction,
      approveAgent: approveAgentFn,
      logout,

      agent,
      agentReady,
      clearAgent: handleClearAgent,
      eoaAddress,

      authDataMap,
      authToken: tgAuthToken,
      telegramRpcOptions,
      telegramClient: tgClient,
      staticClient,
    }),
    [
      user,
      tgLoading,
      eoaLoading,
      tgError,
      eoaError,
      authMethod,
      exchange,
      wallets,
      selectedWalletId,
      selectWallet,
      createWallet,
      deleteWallet,
      renameWallet,
      renewAgentWallet,
      refreshWallets,
      packs,
      createWalletPack,
      addPackLabel,
      modifyPackLabel,
      removePackLabel,
      createTwap,
      modifyTwap,
      cancelTwap,
      createScale,
      cancelScale,
      loginModalOpen,
      openLoginModal,
      closeLoginModal,
      loginTelegram,
      connectEoa,
      signEvmTransaction,
      approveAgentFn,
      logout,
      agent,
      agentReady,
      handleClearAgent,
      eoaAddress,
      authDataMap,
      tgAuthToken,
      telegramRpcOptions,
      tgClient,
      staticClient,
    ],
  );

  return (
    <HypurrConnectContext.Provider value={value}>
      {children}
    </HypurrConnectContext.Provider>
  );
}
