import { Address, Client, isAddress, zeroAddress } from "viem"
import { readContract } from "viem/actions"
import { BigNumber, BigNumberSource } from "../utils/BigNumber"
import {
  KnownChainId,
  KnownTokenId,
  _allKnownEVMTokens,
} from "../utils/types/knownIds"
import { EVMAddress } from "../xlinkSdkUtils/types"
import { SDKGlobalContext } from "../xlinkSdkUtils/types.internal"
import { BridgeConfigAbi } from "./contractAbi/bridgeConfig"
import {
  EVMEndpointContract,
  EVMOnChainAddresses,
  evmContractAddresses,
} from "./evmContractAddresses"

const CONTRACT_COMMON_NUMBER_SCALE = 18
export const numberFromSolidityContractNumber = (
  num: bigint,
  decimals?: number,
): BigNumber => {
  return BigNumber.leftMoveDecimals(
    decimals ?? CONTRACT_COMMON_NUMBER_SCALE,
    num,
  )
}
export const numberToSolidityContractNumber = (
  num: BigNumberSource,
  decimals?: number,
): bigint => {
  return BigNumber.toBigInt(
    {},
    BigNumber.rightMoveDecimals(decimals ?? CONTRACT_COMMON_NUMBER_SCALE, num),
  )
}

export async function getEVMContractCallInfo(
  sdkContext: SDKGlobalContext,
  chainId: KnownChainId.EVMChain,
): Promise<
  | undefined
  | {
      client: Client
      bridgeEndpointContractAddress: Address
      nativeBridgeEndpointContractAddress?: Address
      registryContractAddress?: Address
      timeLockContractAddress?: Address
    }
> {
  const addresses = await getAllAddresses(sdkContext, chainId)
  if (addresses == null) return

  const bridgeEndpointContractAddress =
    addresses.onChainAddresses?.[EVMEndpointContract.BridgeEndpoint] ??
    addresses.localAddresses[EVMEndpointContract.BridgeEndpoint]
  if (bridgeEndpointContractAddress == null) return

  const nativeBridgeEndpointContractAddress =
    addresses.onChainAddresses?.[EVMEndpointContract.NativeBridgeEndpoint] ??
    addresses.localAddresses[EVMEndpointContract.NativeBridgeEndpoint]
  const registryContractAddress =
    addresses.onChainAddresses?.[EVMEndpointContract.Registry] ??
    addresses.localAddresses[EVMEndpointContract.Registry]
  const timeLockContractAddress =
    addresses.onChainAddresses?.[EVMEndpointContract.TimeLock] ??
    addresses.localAddresses[EVMEndpointContract.TimeLock]

  return {
    client: addresses.client,
    bridgeEndpointContractAddress,
    nativeBridgeEndpointContractAddress,
    registryContractAddress,
    timeLockContractAddress,
  }
}

export async function getEVMTokenContractInfo(
  sdkContext: SDKGlobalContext,
  chainId: KnownChainId.EVMChain,
  tokenId: KnownTokenId.EVMToken,
): Promise<
  | undefined
  | {
      client: Client
      tokenContractAddress: Address
    }
> {
  const addresses = await getAllAddresses(sdkContext, chainId)
  if (addresses == null) return

  const tokenContractAddress =
    addresses.onChainAddresses?.[tokenId] ?? addresses.localAddresses[tokenId]

  if (tokenContractAddress == null) return

  return {
    client: addresses.client,
    tokenContractAddress,
  }
}

export async function getEVMToken(
  sdkContext: SDKGlobalContext,
  chain: KnownChainId.EVMChain,
  tokenAddress: EVMAddress,
): Promise<undefined | KnownTokenId.EVMToken> {
  const addresses = await getAllAddresses(sdkContext, chain)
  if (addresses == null) return

  tokenAddress = tokenAddress.toLowerCase() as EVMAddress
  return Object.values(_allKnownEVMTokens).find(
    token =>
      (
        addresses.onChainAddresses?.[token] ?? addresses.localAddresses?.[token]
      )?.toLowerCase() === tokenAddress,
  )
}

export async function getAllAddresses(
  sdkContext: SDKGlobalContext,
  chainId: KnownChainId.EVMChain,
): Promise<
  | undefined
  | {
      client: Client
      onChainAddresses?: EVMOnChainAddresses
      localAddresses: EVMOnChainAddresses
    }
> {
  const client = sdkContext.evm.viemClients[chainId]
  const localAddresses = evmContractAddresses[chainId]
  const configContractAddress = localAddresses[EVMEndpointContract.BridgeConfig]

  if (client == null) return

  let onChainAddresses: undefined | EVMOnChainAddresses
  if (configContractAddress != null) {
    onChainAddresses = await getOnChainConfigs(
      sdkContext,
      chainId,
      configContractAddress,
    )
  }

  return {
    client,
    onChainAddresses,
    localAddresses,
  }
}

const getOnChainConfigs = async (
  sdkContext: SDKGlobalContext,
  chain: KnownChainId.EVMChain,
  configContractAddress: Address,
): Promise<undefined | EVMOnChainAddresses> => {
  const cache = sdkContext.evm.onChainConfigCache
  const cacheKey = `${chain}:${configContractAddress}`

  if (cache != null) {
    const cachedPromise = cache.get(cacheKey)
    if (cachedPromise != null) return cachedPromise
  }

  const client = sdkContext.evm.viemClients[chain]
  if (client == null) return

  const promise = _getOnChainConfigsImpl(
    client,
    chain,
    configContractAddress,
  ).catch(err => {
    queueMicrotask(() => {
      if (cache != null && promise === cache.get(cacheKey)) {
        cache.delete(cacheKey)
      }
    })
    throw err
  })
  if (cache != null) {
    cache.set(cacheKey, promise)
  }
  return promise
}

const _getOnChainConfigsImpl = async (
  client: Client,
  chain: KnownChainId.EVMChain,
  configContractAddress: Address,
): Promise<EVMOnChainAddresses> => {
  const configs = await readContract(client, {
    abi: BridgeConfigAbi,
    address: configContractAddress,
    functionName: "getConfigs",
    args: [
      [
        ONCHAIN_CONFIG_KEY.ENDPOINT,
        ONCHAIN_CONFIG_KEY.REGISTRY,
        ONCHAIN_CONFIG_KEY.TIMELOCK,
        ONCHAIN_CONFIG_KEY.TOKEN_ABTC,
        ONCHAIN_CONFIG_KEY.TOKEN_ALEX,
        ONCHAIN_CONFIG_KEY.TOKEN_ATALEX,
        ONCHAIN_CONFIG_KEY.TOKEN_LISTX,
        ONCHAIN_CONFIG_KEY.TOKEN_USDT,
        ONCHAIN_CONFIG_KEY.TOKEN_BTC,
        ONCHAIN_CONFIG_KEY.TOKEN_LUNR,
        ONCHAIN_CONFIG_KEY.TOKEN_SKO,
        ONCHAIN_CONFIG_KEY.TOKEN_SUSDT,
        ONCHAIN_CONFIG_KEY.TOKEN_UBTC,
        ONCHAIN_CONFIG_KEY.TOKEN_WUBTC,
        ONCHAIN_CONFIG_KEY.TOKEN_DB20,
        ONCHAIN_CONFIG_KEY.TOKEN_DOG,
        ONCHAIN_CONFIG_KEY.ENDPOINT_NATIVE,
      ],
    ],
  }).catch(err => {
    console.groupCollapsed(
      `Failed to read on-chain configs from ${configContractAddress} (${chain})`,
    )
    console.debug(err)
    console.groupEnd()
    return null
  })

  if (configs == null) {
    return {}
  }

  const EVMToken = KnownTokenId.EVM
  return {
    [EVMEndpointContract.BridgeEndpoint]: maybeAddress(configs[0]),
    [EVMEndpointContract.Registry]: maybeAddress(configs[1]),
    [EVMEndpointContract.TimeLock]: maybeAddress(configs[2]),
    [EVMToken.aBTC]: maybeAddress(configs[3]),
    [EVMToken.ALEX]: maybeAddress(configs[4]),
    [EVMToken.vLiALEX]: maybeAddress(configs[5]),
    [EVMToken.vLiSTX]: maybeAddress(configs[6]),
    // prettier-ignore
    [EVMToken.USDC]:
      chain === KnownChainId.EVM.Base
        ? maybeAddress(configs[7])
        : undefined,
    // prettier-ignore
    [EVMToken.USDT]:
      chain === KnownChainId.EVM.Ethereum ||
      chain === KnownChainId.EVM.BSC
        ? maybeAddress(configs[7])
        : undefined,
    // prettier-ignore
    [EVMToken.WBTC]:
      chain === KnownChainId.EVM.Ethereum ||
      chain === KnownChainId.EVM.Arbitrum
        ? maybeAddress(configs[8])
        : undefined,
    // prettier-ignore
    [EVMToken.BTCB]:
      chain === KnownChainId.EVM.BSC
        ? maybeAddress(configs[8])
        : undefined,
    // prettier-ignore
    [EVMToken.cbBTC]:
      chain === KnownChainId.EVM.Base
        ? maybeAddress(configs[8])
        : undefined,
    [EVMToken.LUNR]: maybeAddress(configs[9]),
    [EVMToken.SKO]: maybeAddress(configs[10]),
    [EVMToken.sUSDT]: maybeAddress(configs[11]),
    [EVMToken.uBTC]: maybeAddress(configs[12]),
    [EVMToken.wuBTC]: maybeAddress(configs[13]),
    [EVMToken.DB20]: maybeAddress(configs[14]),
    [EVMToken.DOG]: maybeAddress(configs[15]),
    [EVMEndpointContract.NativeBridgeEndpoint]: maybeAddress(configs[16]),
  }
}
function maybeAddress(value: string | null): Address | undefined {
  if (value == null) return undefined
  if (value === "") return undefined
  if (!isAddress(value)) return undefined
  if (value === zeroAddress) return undefined
  return value
}

enum ONCHAIN_CONFIG_KEY {
  /**
   * https://t.me/c/1599543687/57298
   */
  ENDPOINT = "ENDPOINT",
  REGISTRY = "REGISTRY",
  TOKEN_ABTC = "TOKEN_ABTC",
  TOKEN_ALEX = "TOKEN_ALEX",
  TOKEN_ATALEX = "TOKEN_ATALEX",
  TOKEN_LISTX = "TOKEN_LISTX",
  TOKEN_USDT = "TOKEN_USDT",
  TOKEN_BTC = "TOKEN_BTC",
  TOKEN_LUNR = "TOKEN_LUNR",
  TOKEN_SKO = "TOKEN_SKO",
  TOKEN_SUSDT = "TOKEN_SUSDT",
  TOKEN_DB20 = "TOKEN_DB20",
  TOKEN_DOG = "TOKEN_DOG",
  TIMELOCK = "TIMELOCK",
  MULTISIG = "MULTISIG",
  MIGRATE = "MIGRATE",
  MIGRATE_BOB = "MIGRATE_BOB",
  MIGRATE_BOB_L2 = "MIGRATE_BOB_L2",
  MIGRATE_BOB_L2_S = "MIGRATE_BOB_L2_S",

  // https://github.com/xlink-network/xlink/blob/9e6e268d820f2f9756ca15a36f8580e4f98c087e/packages/contracts/bridge-solidity/scripts/params.ts
  ENDPOINT_NATIVE = "ENDPOINT_NATIVE",

  // https://github.com/xlink-network/xlink/pull/299/commits/22b23c9ff3ea65eeb7c632db4255afe803f97fef#diff-8302902f9863ee3c7928a0fa6eb6ca22edd10f5553708459cdd072c1ea3ef696
  TOKEN_UBTC = "TOKEN_UBTC",
  TOKEN_WUBTC = "TOKEN_WUBTC",
}
