import { callReadOnlyFunction } from "@stacks/transactions"
import { CallReadOnlyFunctionFn, unwrapResponse } from "clarity-codegen"
import { readContract } from "viem/actions"
import {
  getBRC20SupportedRoutes,
  getRunesSupportedRoutes,
} from "../metaUtils/xlinkContractHelpers"
import { contractAssignedChainIdFromKnownChain } from "../stacksUtils/crossContractDataMapping"
import {
  getTerminatingStacksTokenContractAddress,
  stxTokenContractAddresses,
} from "../stacksUtils/stxContractAddresses"
import {
  executeReadonlyCallXLINK,
  getStacksContractCallInfo,
  getStacksTokenContractInfo,
  numberFromStacksContractNumber,
} from "../stacksUtils/xlinkContractHelpers"
import { BigNumber } from "../utils/BigNumber"
import {
  IsSupportedFn,
  KnownRoute_FromEVM_ToStacks,
  KnownRoute_FromStacks_ToEVM,
} from "../utils/buildSupportedRoutes"
import { props } from "../utils/promiseHelpers"
import { assertExclude, checkNever } from "../utils/typeHelpers"
import { TransferProphet } from "../utils/types/TransferProphet"
import {
  _allNoLongerSupportedEVMChains,
  KnownChainId,
  KnownTokenId,
} from "../utils/types/knownIds"
import { SDKGlobalContext } from "../xlinkSdkUtils/types.internal"
import { nativeCurrencyAddress } from "./addressHelpers"
import { BridgeEndpointAbi } from "./contractAbi/bridgeEndpoint"
import { BridgeRegistryAbi } from "./contractAbi/bridgeRegistry"
import {
  getEVMContractCallInfo,
  getEVMTokenContractInfo,
  numberFromSolidityContractNumber,
} from "./xlinkContractHelpers"

export const getEvm2StacksFeeInfo = async (
  ctx: SDKGlobalContext,
  route: KnownRoute_FromEVM_ToStacks,
): Promise<undefined | TransferProphet> => {
  const stacksContractCallInfo = getStacksContractCallInfo(
    route.toChain,
    "cross-peg-in-endpoint-v2-04",
  )
  const evmContractCallInfo = await getEVMContractCallInfo(ctx, route.fromChain)
  const evmTokenContractCallInfo = await getEVMTokenContractInfo(
    ctx,
    route.fromChain,
    route.fromToken,
  )
  if (
    stacksContractCallInfo == null ||
    evmContractCallInfo == null ||
    evmTokenContractCallInfo == null
  ) {
    return
  }

  if (evmTokenContractCallInfo.tokenContractAddress === nativeCurrencyAddress) {
    return getEvm2StacksNativeBridgeFeeInfo(ctx, route)
  }

  const executeOptions = {
    deployerAddress: stacksContractCallInfo.deployerAddress,
    callReadOnlyFunction: (callOptions =>
      callReadOnlyFunction({
        ...callOptions,
        network: stacksContractCallInfo.network,
      })) satisfies CallReadOnlyFunctionFn,
  }

  const { client, tokenContractAddress } = evmTokenContractCallInfo

  const registryAddr =
    evmContractCallInfo.registryContractAddress ??
    (await readContract(client, {
      abi: BridgeEndpointAbi,
      address: evmContractCallInfo.bridgeEndpointContractAddress,
      functionName: "registry",
    }))

  const resp = await props({
    isApproved: readContract(client, {
      abi: BridgeRegistryAbi,
      address: registryAddr,
      functionName: "APPROVED_TOKEN",
    }).then(key =>
      readContract(client, {
        abi: BridgeRegistryAbi,
        address: registryAddr,
        functionName: "hasRole",
        args: [key, tokenContractAddress],
      }),
    ),
    feeRate: readContract(client, {
      abi: BridgeRegistryAbi,
      address: registryAddr,
      functionName: "feePctPerToken",
      args: [tokenContractAddress],
    }).then(numberFromSolidityContractNumber),
    minFeeAmount: readContract(client, {
      abi: BridgeRegistryAbi,
      address: registryAddr,
      functionName: "minFeePerToken",
      args: [tokenContractAddress],
    }).then(numberFromSolidityContractNumber),
    minAmount: readContract(client, {
      abi: BridgeRegistryAbi,
      address: registryAddr,
      functionName: "minAmountPerToken",
      args: [tokenContractAddress],
    }).then(numberFromSolidityContractNumber),
    maxAmount: readContract(client, {
      abi: BridgeRegistryAbi,
      address: registryAddr,
      functionName: "maxAmountPerToken",
      args: [tokenContractAddress],
    }).then(numberFromSolidityContractNumber),
    isPaused: executeReadonlyCallXLINK(
      stacksContractCallInfo.contractName,
      "get-paused",
      {},
      executeOptions,
    ),
  })

  if (!resp.isApproved) return undefined

  const minAmount = BigNumber.max([resp.minAmount, resp.minFeeAmount])
  const maxAmount = BigNumber.min([resp.maxAmount])

  return {
    isPaused: resp.isPaused,
    bridgeToken: route.fromToken,
    fees: [
      {
        type: "rate",
        token: route.fromToken,
        rate: resp.feeRate,
        minimumAmount: resp.minFeeAmount,
      },
    ],
    minBridgeAmount: BigNumber.isZero(minAmount) ? null : minAmount,
    maxBridgeAmount: BigNumber.isZero(maxAmount) ? null : maxAmount,
  }
}

const getEvm2StacksNativeBridgeFeeInfo = async (
  ctx: SDKGlobalContext,
  route: KnownRoute_FromEVM_ToStacks,
): Promise<undefined | TransferProphet> => {
  const stacksContractCallInfo = getStacksContractCallInfo(
    route.toChain,
    "cross-peg-in-endpoint-v2-04",
  )
  const evmContractCallInfo = await getEVMContractCallInfo(ctx, route.fromChain)
  if (
    stacksContractCallInfo == null ||
    evmContractCallInfo?.nativeBridgeEndpointContractAddress == null
  ) {
    return
  }

  const executeOptions = {
    deployerAddress: stacksContractCallInfo.deployerAddress,
    callReadOnlyFunction: (callOptions =>
      callReadOnlyFunction({
        ...callOptions,
        network: stacksContractCallInfo.network,
      })) satisfies CallReadOnlyFunctionFn,
  }

  const resp = await props({
    isPaused: executeReadonlyCallXLINK(
      stacksContractCallInfo.contractName,
      "get-paused",
      {},
      executeOptions,
    ),
  })

  return {
    isPaused: resp.isPaused,
    bridgeToken: route.fromToken,
    fees: [],
    minBridgeAmount: null,
    maxBridgeAmount: null,
  }
}

export const getStacks2EvmFeeInfo = async (
  route: KnownRoute_FromStacks_ToEVM,
): Promise<undefined | TransferProphet> => {
  const stacksContractCallInfo = getStacksContractCallInfo(
    route.fromChain,
    "cross-peg-out-endpoint-v2-01",
  )
  const stacksTokenContractCallInfo = getStacksTokenContractInfo(
    route.fromChain,
    route.fromToken,
  )
  const toChainId = contractAssignedChainIdFromKnownChain(route.toChain)
  if (stacksContractCallInfo == null || stacksTokenContractCallInfo == null) {
    return
  }

  const terminatingStacksTokenAddress =
    getTerminatingStacksTokenContractAddress(route) ??
    stacksTokenContractCallInfo

  const executeOptions = {
    deployerAddress: stacksContractCallInfo.deployerAddress,
    callReadOnlyFunction: (callOptions =>
      callReadOnlyFunction({
        ...callOptions,
        network: stacksContractCallInfo.network,
      })) satisfies CallReadOnlyFunctionFn,
  }

  const tokenConf = await Promise.all([
    executeReadonlyCallXLINK(
      stacksContractCallInfo.contractName,
      "get-approved-pair-or-fail",
      {
        pair: {
          token: `${terminatingStacksTokenAddress.deployerAddress}.${terminatingStacksTokenAddress.contractName}`,
          "chain-id": toChainId,
        },
      },
      executeOptions,
    ),
    executeReadonlyCallXLINK(
      stacksContractCallInfo.contractName,
      "get-paused",
      {},
      executeOptions,
    ),
  ]).then(([resp, isPaused]) => {
    if (resp.type !== "success") return undefined

    return {
      ...unwrapResponse(resp),
      isPaused,
    }
  })

  if (tokenConf == null) return undefined

  const feeRate = numberFromStacksContractNumber(tokenConf.fee)
  const minFee = numberFromStacksContractNumber(tokenConf["min-fee"])
  const reserve = numberFromStacksContractNumber(tokenConf.reserve)

  const minAmount = BigNumber.max([
    numberFromStacksContractNumber(tokenConf["min-amount"]),
    minFee,
  ])
  const maxAmount = BigNumber.min([
    numberFromStacksContractNumber(tokenConf["max-amount"]),
    reserve,
  ])

  return {
    isPaused: tokenConf.isPaused || tokenConf.approved === false,
    bridgeToken: route.fromToken,
    fees: [
      {
        type: "rate",
        token: route.fromToken,
        rate: feeRate,
        minimumAmount: minFee,
      },
    ],
    minBridgeAmount: BigNumber.isZero(minAmount) ? null : minAmount,
    maxBridgeAmount: BigNumber.isZero(maxAmount) ? null : maxAmount,
  }
}

export async function fromCorrespondingStacksToken(
  toChain: KnownChainId.EVMChain,
  stacksToken: KnownTokenId.StacksToken,
): Promise<KnownTokenId.EVMToken[]> {
  const EVMToken = KnownTokenId.EVM
  const StacksToken = KnownTokenId.Stacks

  const restEVMTokenPossibilities = assertExclude.i<KnownTokenId.EVMToken>()

  if (stacksToken === StacksToken.sLUNR) {
    return [EVMToken.LUNR]
  }
  assertExclude(restEVMTokenPossibilities, EVMToken.LUNR)

  if (stacksToken === StacksToken.ALEX) {
    return [EVMToken.ALEX]
  }
  assertExclude(restEVMTokenPossibilities, EVMToken.ALEX)

  if (stacksToken === StacksToken.sSKO) {
    return [EVMToken.SKO]
  }
  assertExclude(restEVMTokenPossibilities, EVMToken.SKO)

  if (stacksToken === StacksToken.vLiSTX) {
    return [EVMToken.vLiSTX]
  }
  assertExclude(restEVMTokenPossibilities, EVMToken.vLiSTX)

  if (stacksToken === StacksToken.vLiALEX) {
    return [EVMToken.vLiALEX]
  }
  assertExclude(restEVMTokenPossibilities, EVMToken.vLiALEX)

  if (stacksToken === StacksToken.sUSDT) {
    return [EVMToken.sUSDT, EVMToken.USDT, EVMToken.USDC]
  }
  assertExclude(restEVMTokenPossibilities, EVMToken.sUSDT)
  assertExclude(restEVMTokenPossibilities, EVMToken.USDT)
  assertExclude(restEVMTokenPossibilities, EVMToken.USDC)
  if (stacksToken === StacksToken.aBTC) {
    return [EVMToken.aBTC, EVMToken.WBTC, EVMToken.BTCB, EVMToken.cbBTC]
  }
  assertExclude(restEVMTokenPossibilities, EVMToken.aBTC)
  assertExclude(restEVMTokenPossibilities, EVMToken.WBTC)
  assertExclude(restEVMTokenPossibilities, EVMToken.BTCB)
  assertExclude(restEVMTokenPossibilities, EVMToken.cbBTC)

  if (stacksToken === StacksToken.uBTC) {
    return [EVMToken.uBTC, EVMToken.wuBTC]
  }
  assertExclude(restEVMTokenPossibilities, EVMToken.uBTC)
  assertExclude(restEVMTokenPossibilities, EVMToken.wuBTC)

  if (stacksToken === StacksToken.DB20) {
    return [EVMToken.DB20]
  }
  assertExclude(restEVMTokenPossibilities, EVMToken.DB20)

  if (stacksToken === StacksToken.DOG) {
    return [EVMToken.DOG]
  }
  assertExclude(restEVMTokenPossibilities, EVMToken.DOG)

  checkNever(restEVMTokenPossibilities)
  return []
}
export async function toCorrespondingStacksToken(
  evmToken: KnownTokenId.EVMToken,
): Promise<undefined | KnownTokenId.StacksToken> {
  const EVMToken = KnownTokenId.EVM
  const StacksToken = KnownTokenId.Stacks

  switch (evmToken) {
    case EVMToken.LUNR:
      return StacksToken.sLUNR
    case EVMToken.ALEX:
      return StacksToken.ALEX
    case EVMToken.SKO:
      return StacksToken.sSKO
    case EVMToken.vLiSTX:
      return StacksToken.vLiSTX
    case EVMToken.vLiALEX:
      return StacksToken.vLiALEX
    case EVMToken.sUSDT:
    case EVMToken.USDT:
    case EVMToken.USDC:
      return StacksToken.sUSDT
    case EVMToken.aBTC:
    case EVMToken.BTCB:
    case EVMToken.WBTC:
    case EVMToken.cbBTC:
      return StacksToken.aBTC
    case EVMToken.uBTC:
    case EVMToken.wuBTC:
      return StacksToken.uBTC
    case EVMToken.DB20:
      return StacksToken.DB20
    case EVMToken.DOG:
      return StacksToken.DOG
    default:
      checkNever(evmToken)
      return
  }
}

export const isSupportedEVMRoute: IsSupportedFn = async (ctx, route) => {
  const { fromChain, fromToken, toChain, toToken } = route

  if (fromChain === toChain && fromToken === toToken) {
    return false
  }

  if (
    KnownChainId.isEVMChain(fromChain) &&
    _allNoLongerSupportedEVMChains.includes(fromChain)
  ) {
    return false
  }
  if (
    KnownChainId.isEVMChain(toChain) &&
    _allNoLongerSupportedEVMChains.includes(toChain)
  ) {
    return false
  }

  if (
    !KnownChainId.isEVMChain(fromChain) ||
    !KnownTokenId.isEVMToken(fromToken)
  ) {
    return false
  }
  if (!KnownChainId.isKnownChain(toChain)) return false

  const fromContractInfo = await getEVMContractCallInfo(ctx, fromChain)
  if (fromContractInfo == null) return false

  const fromTokenInfo = await getEVMTokenContractInfo(ctx, fromChain, fromToken)
  if (fromTokenInfo == null) return false

  if (fromTokenInfo.tokenContractAddress === nativeCurrencyAddress) {
    if (fromContractInfo.bridgeEndpointContractAddress == null) return false
    if (!KnownChainId.isBitcoinChain(toChain)) return false
  }

  if (KnownChainId.isStacksChain(toChain)) {
    if (!KnownTokenId.isStacksToken(toToken)) return false

    const stacksToken = await toCorrespondingStacksToken(fromToken)
    if (stacksToken == null) return false

    if (stxTokenContractAddresses[stacksToken]?.[toChain] == null) {
      return false
    }

    return stacksToken === toToken
  }

  if (KnownChainId.isEVMChain(toChain)) {
    if (!KnownTokenId.isEVMToken(toToken)) return false

    const toTokenInfo = await getEVMTokenContractInfo(ctx, toChain, toToken)
    if (toTokenInfo == null) return false

    const transitStacksToken = await toCorrespondingStacksToken(fromToken)
    if (transitStacksToken == null) return false

    const toEVMTokens = await fromCorrespondingStacksToken(
      toChain,
      transitStacksToken,
    )
    return toEVMTokens.includes(toToken)
  }

  if (KnownChainId.isBitcoinChain(toChain)) {
    if (!KnownTokenId.isBitcoinToken(toToken)) return false
    const stacksToken = await toCorrespondingStacksToken(fromToken)
    return stacksToken === KnownTokenId.Stacks.aBTC
  }

  if (KnownChainId.isRunesChain(toChain)) {
    if (!KnownTokenId.isRunesToken(toToken)) return false

    const transitStacksToken = await toCorrespondingStacksToken(fromToken)
    if (transitStacksToken == null) return false

    const runesRoutes = await getRunesSupportedRoutes(ctx, toChain)
    return runesRoutes.some(route => route.stacksToken === transitStacksToken)
  }

  if (KnownChainId.isBRC20Chain(toChain)) {
    if (!KnownTokenId.isBRC20Token(toToken)) return false

    const transitStacksToken = await toCorrespondingStacksToken(fromToken)
    if (transitStacksToken == null) return false

    const brc20Routes = await getBRC20SupportedRoutes(ctx, toChain)
    return brc20Routes.some(route => route.stacksToken === transitStacksToken)
  }

  checkNever(toChain)
  return false
}
