import { loginWithWallet } from "./api";

export type WalletKind = "tokenPocket" | "metaMask" | "imToken";

const BNB_CHAIN_ID_HEX = "0x38";
const BNB_CHAIN_PARAMS = {
  chainId: BNB_CHAIN_ID_HEX,
  chainName: "BNB Smart Chain",
  nativeCurrency: { name: "BNB", symbol: "BNB", decimals: 18 },
  rpcUrls: ["https://bsc-dataseed.binance.org"],
  blockExplorerUrls: ["https://bscscan.com"],
};

export type EthereumProvider = {
  isMetaMask?: boolean;
  isTokenPocket?: boolean;
  isImToken?: boolean;
  providers?: EthereumProvider[];
  request: <T = unknown>(args: {
    method: string;
    params?: unknown[];
  }) => Promise<T>;
};

function isAddress(value: unknown): value is string {
  return typeof value === "string" && /^0x[a-fA-F0-9]{40}$/.test(value);
}

function errorText(error: unknown): string {
  if (!error || typeof error !== "object") return String(error ?? "");
  const parts: string[] = [];
  const record = error as Record<string, unknown>;
  for (const key of ["shortMessage", "message", "details"]) {
    const value = record[key];
    if (typeof value === "string") parts.push(value);
  }
  if (record.cause) parts.push(errorText(record.cause));
  return parts.join("\n");
}

function isNoAccountError(error: unknown): boolean {
  return /wallet must has at least one account|wallet must has one account|must have at least one account|no wallet account returned/i.test(
    errorText(error),
  );
}

function normalizeWalletError(error: unknown): Error {
  if (isNoAccountError(error)) return new Error("walletNoAccount");
  if (error instanceof Error) return error;
  const message = errorText(error);
  return new Error(message || "Wallet login failed");
}

async function ensureBnbChain(ethereum: EthereumProvider): Promise<void> {
  const chainId = await ethereum
    .request<string>({ method: "eth_chainId" })
    .catch(() => "");
  if (chainId.toLowerCase() === BNB_CHAIN_ID_HEX) return;

  try {
    await ethereum.request({
      method: "wallet_switchEthereumChain",
      params: [{ chainId: BNB_CHAIN_ID_HEX }],
    });
  } catch (error) {
    const code = (error as { code?: number | string } | null)?.code;
    if (code !== 4902 && code !== "4902") throw error;
    await ethereum.request({
      method: "wallet_addEthereumChain",
      params: [BNB_CHAIN_PARAMS],
    });
  }
}

async function requestInjectedAddress(
  ethereum: EthereumProvider,
): Promise<string> {
  const existingAccounts: unknown[] = await ethereum
    .request<unknown[]>({ method: "eth_accounts" })
    .catch((): unknown[] => []);
  const existingAddress = existingAccounts.find(isAddress);
  if (existingAddress) return existingAddress;

  const requestedAccounts = await ethereum
    .request<unknown[]>({
      method: "eth_requestAccounts",
    })
    .catch((error: unknown): never => {
      throw normalizeWalletError(error);
    });
  const requestedAddress = requestedAccounts.find(isAddress);
  if (!requestedAddress) throw new Error("walletNoAccount");
  return requestedAddress;
}

export function getInjectedEthereum(): EthereumProvider | null {
  if (typeof window === "undefined") return null;
  const maybeWindow = window as typeof window & { ethereum?: EthereumProvider };
  return maybeWindow.ethereum ?? null;
}

export function isTokenPocketBrowser(): boolean {
  if (typeof navigator === "undefined") return false;
  return /tokenpocket|tpwallet/i.test(navigator.userAgent || "");
}

export function isImTokenBrowser(): boolean {
  if (typeof navigator === "undefined") return false;
  return /imtoken/i.test(navigator.userAgent || "");
}

export function getInjectedWallet(kind?: WalletKind): EthereumProvider | null {
  const ethereum = getInjectedEthereum();
  if (!ethereum || !kind) return ethereum;
  const providers = ethereum.providers?.length
    ? ethereum.providers
    : [ethereum];
  const match = providers.find((provider) => {
    if (kind === "metaMask") return provider.isMetaMask;
    if (kind === "tokenPocket") return provider.isTokenPocket;
    if (kind === "imToken") return provider.isImToken;
    return false;
  });
  if (match) return match;

  if (kind === "tokenPocket" && isTokenPocketBrowser()) {
    return providers[0] ?? ethereum;
  }

  if (kind === "imToken" && isImTokenBrowser()) return providers[0] ?? ethereum;

  return null;
}

export function logWalletProviders(): void {
  const ethereum = getInjectedEthereum();
  const list = (
    ethereum?.providers?.length
      ? ethereum.providers
      : ethereum
        ? [ethereum]
        : []
  ).map((p) => ({
    isMetaMask: Boolean(p.isMetaMask),
    isTokenPocket: Boolean(p.isTokenPocket),
    isImToken: Boolean(p.isImToken),
  }));
  console.info("[wallet-login] providers", {
    hasEthereum: Boolean(ethereum),
    count: list.length,
    list,
  });
}

export async function connectInjectedWallet(
  kind?: WalletKind,
): Promise<string> {
  console.info("[wallet-login] start injected connect", { kind });
  logWalletProviders();
  const ethereum = getInjectedWallet(kind);
  if (!ethereum) {
    console.warn("[wallet-login] no injected provider found");
    throw new Error("No injected wallet found");
  }

  console.info("[wallet-login] requesting BNB wallet account…");
  const address = await requestInjectedAddress(ethereum);
  console.info("[wallet-login] injected account", address);

  console.info("[wallet-login] ensuring BNB Chain (0x38)…");
  await ensureBnbChain(ethereum);
  return address;
}

export async function signInWithInjectedWallet(kind?: WalletKind): Promise<{
  token: string;
  wallet: string;
}> {
  const address = await connectInjectedWallet(kind);
  console.info("[wallet-login] requesting backend login for", address);
  const result = await loginWithWallet(address);
  console.info("[wallet-login] logged in, wallet =", result.wallet);
  return result;
}
