import fetch from "node-fetch";
import { WalletMappingInput } from "../types";
import { EVM_CHAIN_TYPES, normalizeRequiredChainType, parseChainStringMap } from "./ChainConfig";
import { IMappingStore } from "./StoreContracts";
import { WalletRegistrationService } from "./WalletRegistrationService";
import { WalletGenerationService } from "./WalletGenerationService";

type Direction = "incoming" | "outgoing" | "all";

interface RpcResponse<T = unknown> {
  result?: T;
  error?: {
    message?: string;
  };
}

export class AlchemyMultiChainService {
  private readonly rpcUrlsByChainType: Record<string, string>;
  private readonly evmChains = EVM_CHAIN_TYPES;

  constructor(
    rpcUrlsRaw: string | undefined,
    private mappingStoreService: IMappingStore,
    private walletRegistrationService: WalletRegistrationService,
    private walletGenerationService: WalletGenerationService
  ) {
    this.rpcUrlsByChainType = parseChainStringMap(rpcUrlsRaw, "ALCHEMY_RPC_URLS");
  }

  async generateWalletForDerivationKey(
    derivationIdentityKey: number,
    chainType: string,
    walletId?: string,
    walletMetadata: Partial<WalletMappingInput> = {}
  ): Promise<{
    user_id: number;
    chain_type: string;
    address: string;
    wallet_id: string | null;
    created: boolean;
    derivation_path: string | null;
    index: number | null;
  }> {
    const normalizedChain = this.normalizeChainType(chainType);
    const existingWallet = (await this.mappingStoreService
      .getByUserId(derivationIdentityKey))
      ?.wallets.find((wallet) => wallet.chain_type === normalizedChain);

    if (existingWallet) {
      return {
        user_id: derivationIdentityKey,
        chain_type: normalizedChain,
        address: existingWallet.address,
        wallet_id: existingWallet.wallet_id || null,
        created: false,
        derivation_path: null,
        index: null,
      };
    }

    const generated = this.walletGenerationService.generate(normalizedChain, derivationIdentityKey);
    const resolvedWalletId = walletId || `${normalizedChain}_${derivationIdentityKey}`;

    await this.walletRegistrationService.registerWallets(derivationIdentityKey, [
      {
        chain_type: normalizedChain,
        address: generated.address,
        wallet_id: resolvedWalletId,
        derivation_index: generated.index,
        ...walletMetadata,
      },
    ]);

    return {
      user_id: derivationIdentityKey,
      chain_type: normalizedChain,
      address: generated.address,
      wallet_id: resolvedWalletId,
      created: true,
      derivation_path: generated.derivation_path,
      index: generated.index,
    };
  }

  async registerWallet(derivationIdentityKey: number, chainType: string, address: string, walletId?: string): Promise<unknown> {
    const normalizedChain = this.normalizeChainType(chainType);
    return this.walletRegistrationService.registerWallets(derivationIdentityKey, [
      {
        chain_type: normalizedChain,
        address,
        wallet_id: walletId || null,
      },
    ]);
  }

  async fetchLatestTransactions(
    chainType: string,
    address: string,
    limit: number,
    direction: Direction,
    contractAddress?: string
  ): Promise<unknown[]> {
    const normalizedChain = this.normalizeChainType(chainType);

    if (normalizedChain === "solana") {
      const signatures = await this.rpcCall<any[]>(normalizedChain, "getSignaturesForAddress", [
        address,
        { limit: Math.max(1, Math.min(limit, 100)) },
      ]);
      return signatures || [];
    }

    if (!this.evmChains.has(normalizedChain)) {
      throw this.badRequest(`Unsupported chain_type '${chainType}'`);
    }

    const categories = ["external", "internal", "erc20"];
    const maxCount = `0x${Math.max(1, Math.min(limit, 1000)).toString(16)}`;
    const common: Record<string, unknown> = {
      fromBlock: "0x0",
      toBlock: "latest",
      category: categories,
      withMetadata: true,
      excludeZeroValue: false,
      maxCount,
    };

    if (contractAddress && contractAddress.trim()) {
      common.contractAddresses = [contractAddress.trim()];
    }

    const calls: Array<Promise<any>> = [];
    if (direction === "incoming" || direction === "all") {
      calls.push(this.rpcCall<any>(normalizedChain, "alchemy_getAssetTransfers", [{ ...common, toAddress: address }]));
    }
    if (direction === "outgoing" || direction === "all") {
      calls.push(this.rpcCall<any>(normalizedChain, "alchemy_getAssetTransfers", [{ ...common, fromAddress: address }]));
    }

    const results = await Promise.all(calls);
    const merged = results.flatMap((item) => item?.transfers || []);
    const deduped = new Map<string, unknown>();
    for (const transfer of merged) {
      const key = `${transfer?.hash || ""}:${transfer?.uniqueId || ""}`;
      deduped.set(key, transfer);
    }

    return Array.from(deduped.values()).slice(0, limit);
  }

  async getTransaction(chainType: string, txId: string): Promise<unknown> {
    const normalizedChain = this.normalizeChainType(chainType);
    if (normalizedChain === "solana") {
      return this.rpcCall(
        normalizedChain,
        "getTransaction",
        [txId, { encoding: "json", maxSupportedTransactionVersion: 0 }]
      );
    }

    if (!this.evmChains.has(normalizedChain)) {
      throw this.badRequest(`Unsupported chain_type '${chainType}'`);
    }

    return this.rpcCall(normalizedChain, "eth_getTransactionByHash", [txId]);
  }

  async getAddressBalance(
    chainType: string,
    address: string,
    contractAddress?: string
  ): Promise<{ chain_type: string; address: string; asset: string; balance: string; raw: unknown }> {
    const normalizedChain = this.normalizeChainType(chainType);

    if (normalizedChain === "solana") {
      if (contractAddress && contractAddress.trim()) {
        const tokenAccounts = await this.rpcCall<any>(
          normalizedChain,
          "getTokenAccountsByOwner",
          [address, { mint: contractAddress.trim() }, { encoding: "jsonParsed" }]
        );

        const amount = (tokenAccounts?.value || []).reduce((sum: bigint, account: any) => {
          const rawAmount = account?.account?.data?.parsed?.info?.tokenAmount?.amount || "0";
          try {
            return sum + BigInt(String(rawAmount));
          } catch {
            return sum;
          }
        }, 0n);

        return {
          chain_type: normalizedChain,
          address,
          asset: contractAddress.trim(),
          balance: amount.toString(10),
          raw: tokenAccounts,
        };
      }

      const nativeBalance = await this.rpcCall<any>(normalizedChain, "getBalance", [address]);
      const lamports = nativeBalance?.value || 0;
      return {
        chain_type: normalizedChain,
        address,
        asset: "SOL",
        balance: String(lamports),
        raw: nativeBalance,
      };
    }

    if (!this.evmChains.has(normalizedChain)) {
      throw this.badRequest(`Unsupported chain_type '${chainType}'`);
    }

    if (contractAddress && contractAddress.trim()) {
      const tokenBalances = await this.rpcCall<any>(
        normalizedChain,
        "alchemy_getTokenBalances",
        [address, [contractAddress.trim()]]
      );
      const tokenBalance = tokenBalances?.tokenBalances?.[0]?.tokenBalance || "0x0";

      return {
        chain_type: normalizedChain,
        address,
        asset: contractAddress.trim(),
        balance: this.hexToDecimalString(tokenBalance),
        raw: tokenBalances,
      };
    }

    const nativeBalance = await this.rpcCall<string>(normalizedChain, "eth_getBalance", [address, "latest"]);
    return {
      chain_type: normalizedChain,
      address,
      asset: normalizedChain === "bsc" ? "BNB" : normalizedChain === "base" ? "ETH" : "ETH",
      balance: this.hexToDecimalString(nativeBalance || "0x0"),
      raw: nativeBalance,
    };
  }

  async getTotalTrackedBalance(
    chainType: string,
    contractAddress?: string
  ): Promise<{ chain_type: string; asset: string; tracked_addresses: number; total_balance: string }> {
    const normalizedChain = this.normalizeChainType(chainType);
    const wallets = await this.mappingStoreService.getWalletsByChainType(normalizedChain);
    let total = 0n;

    for (const wallet of wallets) {
      const balance = await this.getAddressBalance(normalizedChain, wallet.wallet.address, contractAddress);
      try {
        total += BigInt(balance.balance);
      } catch {
        // ignore parse issues per-address and continue aggregating
      }
    }

    return {
      chain_type: normalizedChain,
      asset: contractAddress?.trim() || this.defaultAsset(normalizedChain),
      tracked_addresses: wallets.length,
      total_balance: total.toString(10),
    };
  }

  private normalizeChainType(chainType: string): string {
    try {
      return normalizeRequiredChainType(chainType);
    } catch {
      throw this.badRequest("chain_type is required");
    }
  }

  private defaultAsset(chainType: string): string {
    if (chainType === "solana") {
      return "SOL";
    }

    if (chainType === "bsc") {
      return "BNB";
    }

    return "ETH";
  }

  private async rpcCall<T>(chainType: string, method: string, params: unknown[]): Promise<T> {
    const url = this.rpcUrlsByChainType[chainType];
    if (!url) {
      throw this.badRequest(`Missing Alchemy RPC URL for chain '${chainType}'`);
    }

    const response = await fetch(url, {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({
        jsonrpc: "2.0",
        id: 1,
        method,
        params,
      }),
    });

    if (!response.ok) {
      throw new Error(`Alchemy RPC call failed (${method}) with status ${response.status}`);
    }

    const json = (await response.json()) as RpcResponse<T>;
    if (json.error) {
      throw new Error(json.error.message || `Alchemy RPC call failed for ${method}`);
    }

    return json.result as T;
  }

  private hexToDecimalString(value: string): string {
    if (!value || value === "0x") {
      return "0";
    }

    return BigInt(value).toString(10);
  }

  private badRequest(message: string): Error & { statusCode: number } {
    const error = new Error(message) as Error & { statusCode: number };
    error.statusCode = 400;
    return error;
  }

}
