import {
  DEFAULT_DESTINATION_CHAIN_ID,
  DEFAULT_DESTINATION_TOKEN_ADDR,
  DEFAULT_SOURCE_CHAIN_ID,
  DEFAULT_SOURCE_TOKEN_ADDR,
} from "@src/constants";
import { BridgeTokensDictionary, Chain, Ecosystem, Token } from "@src/models";

export const getDefaultSwapSourceChain = ({
  supportedChains,
}: {
  supportedChains: Chain[];
}): Chain => {
  const defaultSourceChain =
    supportedChains.find(
      (chain) => chain.chainId === DEFAULT_SOURCE_CHAIN_ID,
    ) || supportedChains[0];

  if (!defaultSourceChain) {
    throw new Error(
      `getDefaultSourceChain > couldn't find default source chain`,
    );
  }

  return defaultSourceChain;
};

export const getDefaultDestinationChain = ({
  supportedChains,
}: {
  supportedChains: Chain[];
}): Chain => {
  const defaultTargetChain =
    supportedChains.find(
      (chain) => chain.chainId === DEFAULT_DESTINATION_CHAIN_ID,
    ) || supportedChains[0];

  if (!defaultTargetChain) {
    throw new Error(
      `getDefaultDestinationChain > couldn't find default destination chain`,
    );
  }

  return defaultTargetChain;
};

export const getDefaultSwapTokenForChain = (
  chain: Chain,
  defaultTokenAddress: string,
  sameChain?: boolean,
  sourceTokenId?: string,
) => {
  // First check if there are any supported tokens at all
  const hasAnySupportedTokens = chain.tokens.some((token) => token.supported);
  if (!hasAnySupportedTokens) {
    return undefined;
  }

  const defaultToken = chain.tokens.find(
    (token) =>
      token.address.toLowerCase() === defaultTokenAddress.toLowerCase() &&
      token.supported,
  );

  if (defaultToken) {
    return defaultToken;
  }

  const fallbackToken = chain.tokens.find((token) =>
    sameChain
      ? token.supported && token.tokenId !== sourceTokenId
      : token.supported,
  );

  if (fallbackToken) {
    return fallbackToken;
  }

  return undefined;
};

export const getDefaultBridgeTokenForChain = ({
  chain,
  bridgeTokensDictionary,
}: {
  chain: Chain | undefined;
  bridgeTokensDictionary: BridgeTokensDictionary;
}) => {
  if (!chain) {
    return undefined;
  }

  const xswapToken = chain.tokens.find((token) => token.tokenId === "XSWAP");

  if (xswapToken) {
    return xswapToken;
  }

  const fallbackTokenAddress = Object.keys(
    bridgeTokensDictionary[chain.chainId] || {},
  )[0];

  return chain.tokens.find(
    (token) =>
      token.address.toLowerCase() === fallbackTokenAddress?.toLowerCase(),
  );
};

const isSwapSourceValid = (sourceChain: Chain) => sourceChain.swapSupported;

const isSwapTargetChainValid = ({
  sourceChain,
  targetChain,
}: {
  sourceChain: Chain | undefined;
  targetChain: Chain;
}) => {
  if (!sourceChain) {
    throw new Error(`isSwapTargetChainValid > sourceChain should be defined.`);
  }

  return (
    targetChain.swapSupported &&
    sourceChain.supportedDstForSwap?.includes(targetChain.chainId)
  );
};

const isBridgeSourceValid = (sourceChain: Chain | undefined) =>
  !sourceChain || sourceChain.bridgeSupported;

const isBridgeTargetValid = ({
  sourceChain,
  targetChain,
  sourceToken,
}: {
  sourceChain: Chain | undefined;
  targetChain: Chain | undefined;
  sourceToken: Token | undefined;
}) => {
  // We set it to a valid state initially.
  let isSupportedDstForBridge = true;
  let isSameToken = true;
  let isSameChain = false;

  if (sourceChain && targetChain) {
    isSupportedDstForBridge = !!sourceChain.supportedDstForBridge?.includes(
      targetChain.chainId,
    );
  }

  if (sourceToken && targetChain) {
    // Bridge target chain is invalid when it doesn't have the same token available as selected in the source.
    isSameToken = !!targetChain.tokens.find(
      (token) => token.tokenId === sourceToken.tokenId,
    );
  }

  if (sourceChain && targetChain) {
    // @TODO use bridge_tokens collection
    // See: https://github.com/xswap-link/xswap-sdk/pull/200#discussion_r1905586346
    isSameChain = sourceChain.chainId === targetChain.chainId;
  }

  return isSameToken && !isSameChain && isSupportedDstForBridge;
};

type MapToValidPathParams = {
  source: { chain: Chain | undefined; token: Token | undefined };
  target: { chain: Chain | undefined; token: Token | undefined };
  type: "SWAP" | "BRIDGE";
  supportedChains: Chain[];
  bridgeTokensDictionary: BridgeTokensDictionary;
};

const mapToValidPathSwap = ({
  source,
  target,
  supportedChains,
}: Omit<MapToValidPathParams, "type">) => {
  let newSourceChain: Chain | undefined =
    source.chain || getDefaultSwapSourceChain({ supportedChains });
  let newTargetChain: Chain | undefined =
    target.chain || getDefaultDestinationChain({ supportedChains });
  let newSourceToken: Token | undefined =
    source.token ||
    getDefaultSwapTokenForChain(newSourceChain, DEFAULT_SOURCE_TOKEN_ADDR);
  let newTargetToken: Token | undefined =
    target.token ||
    getDefaultSwapTokenForChain(
      newTargetChain,
      DEFAULT_DESTINATION_TOKEN_ADDR,
      newSourceChain.chainId === newTargetChain.chainId,
      newSourceToken?.tokenId,
    );

  if (
    !newSourceChain ||
    !newTargetChain ||
    !newSourceToken ||
    !newTargetToken
  ) {
    // This should never occur if our app is configured properly.
    return {
      source: { chain: undefined, token: undefined },
      target: { chain: undefined, token: undefined },
    };
  }

  if (!isSwapSourceValid(newSourceChain)) {
    newSourceChain = getDefaultSwapSourceChain({ supportedChains });
    // We can't have dangling token from a different chain.
    newSourceToken = undefined;

    if (!isSwapSourceValid(newSourceChain)) {
      // This should never occur if our app is configured properly.
      console.error("[WRONG CONFIG] default swap source chain is invalid.");

      // Find any chain that is valid.
      newSourceChain = supportedChains.find((chain) =>
        isSwapSourceValid(chain),
      );
      if (!newSourceChain) {
        throw new Error(
          "mapToValidSwap > None of the chains supports swap. Couldn't set source chain.",
        );
      }
    }
  }

  if (
    !isSwapTargetChainValid({
      sourceChain: newSourceChain,
      targetChain: newTargetChain,
    })
  ) {
    newTargetChain = getDefaultDestinationChain({ supportedChains });
    // We can't have dangling token from a different chain.
    newTargetToken = undefined;

    if (
      !isSwapTargetChainValid({
        sourceChain: newSourceChain,
        targetChain: newTargetChain,
      })
    ) {
      // This should never occur if our app is configured properly.
      console.error("[WRONG CONFIG] default swap target chain is invalid.");

      // Find any chain that is valid.
      newTargetChain = supportedChains.find((chain) =>
        isSwapTargetChainValid({
          sourceChain: newSourceChain,
          targetChain: chain,
        }),
      );
      if (!newTargetChain) {
        throw new Error(
          "mapToValidSwap > None of the chains supports swap. Couldn't set target chain.",
        );
      }
    }
  }

  // Setting tokens
  // validate if the token is supported in the new source chain
  newSourceToken = newSourceChain.tokens.find(
    (token) => token.tokenId === newSourceToken?.tokenId && token.supported,
  );
  newTargetToken = newTargetChain.tokens.find(
    (token) => token.tokenId === newTargetToken?.tokenId && token.supported,
  );

  if (!newSourceToken) {
    newSourceToken = getDefaultSwapTokenForChain(
      newSourceChain,
      DEFAULT_SOURCE_TOKEN_ADDR,
    );
  }

  if (!newTargetToken || newSourceToken === newTargetToken) {
    newTargetToken = getDefaultSwapTokenForChain(
      newTargetChain,
      DEFAULT_DESTINATION_TOKEN_ADDR,
      newSourceChain.chainId === newTargetChain.chainId,
      newSourceToken?.tokenId,
    );
  }

  return {
    source: { chain: newSourceChain, token: newSourceToken },
    target: { chain: newTargetChain, token: newTargetToken },
  };
};

const mapToValidPathBridge = ({
  source,
  target,
  supportedChains,
  bridgeTokensDictionary,
}: Omit<MapToValidPathParams, "type">) => {
  let newSourceChain = source.chain;
  let newTargetChain = target.chain;
  let newSourceToken = source.token;
  let newTargetToken = target.token;

  if (!isBridgeSourceValid(newSourceChain)) {
    newSourceChain = undefined;
    newSourceToken = undefined;

    if (!isBridgeSourceValid(newSourceChain)) {
      // This should never occur if our app is configured properly.
      console.error("[WRONG CONFIG] default bridge source chain is invalid.");

      // Find any chain that is valid.
      newSourceChain = supportedChains.find((chain) =>
        isBridgeSourceValid(chain),
      );
      if (!newSourceChain) {
        throw new Error(
          "mapToValidSwap > None of the chains supports bridge. Couldn't set source chain.",
        );
      }
    }
  }

  if (
    !isBridgeTargetValid({
      sourceChain: newSourceChain,
      targetChain: newTargetChain,
      sourceToken: newSourceToken,
    })
  ) {
    // We can't have dangling token from a different chain.
    newTargetToken = undefined;
    if (
      !isBridgeTargetValid({
        sourceChain: newSourceChain,
        targetChain: newTargetChain,
        sourceToken: newSourceToken,
      })
    ) {
      newTargetChain = undefined;
    }
  }

  // Setting tokens
  // check if the source token is bridgeable anywhere
  if (newSourceToken && newSourceChain) {
    // First validate if the token is supported in the source chain
    newSourceToken = newSourceChain.tokens.find(
      (token) => token.tokenId === newSourceToken?.tokenId && token.supported,
    );

    if (newSourceToken) {
      const isBridgeable =
        Object.keys(
          bridgeTokensDictionary[newSourceChain.chainId]?.[
            newSourceChain.ecosystem === Ecosystem.SOLANA
              ? newSourceToken.address
              : newSourceToken.address.toLowerCase()
          ] || {},
        ).length > 0;

      if (!isBridgeable) {
        newSourceToken = undefined;
      }
    }
  }

  // Validate if target token is supported in the target chain
  if (newTargetToken && newTargetChain) {
    newTargetToken = newTargetChain.tokens.find(
      (token) => token.tokenId === newTargetToken?.tokenId && token.supported,
    );
  }

  if (!newSourceToken) {
    const defaultToken = getDefaultBridgeTokenForChain({
      chain: newSourceChain,
      bridgeTokensDictionary,
    });
    if (defaultToken && newSourceChain && newTargetChain) {
      const isDefaultTokenBridgeable =
        !!bridgeTokensDictionary[newSourceChain.chainId]?.[
          defaultToken.address.toLowerCase()
        ]?.[newTargetChain.chainId];

      if (isDefaultTokenBridgeable) {
        newSourceToken = defaultToken;
      } else {
        newSourceToken = undefined;
      }
    } else {
      // Default was not found, so let's keep it unset.
      newSourceToken = undefined;
    }
  }

  if (newSourceToken?.tokenId !== newTargetToken?.tokenId) {
    newTargetToken = undefined;
  }

  if (!newTargetToken) {
    if (newSourceToken && newSourceChain && newTargetChain) {
      const correspondingTokenAddress =
        bridgeTokensDictionary[newSourceChain.chainId]?.[
          newSourceChain.ecosystem === Ecosystem.SOLANA
            ? newSourceToken.address
            : newSourceToken.address.toLowerCase()
        ]?.[newTargetChain.chainId];

      const correspondingToken = newTargetChain.tokens.find(
        (token) =>
          token.address.toLowerCase() ===
          correspondingTokenAddress?.toLowerCase(),
      );
      newTargetToken = correspondingToken;
    } else {
      // If source token is not defined, destination token can't be defined as well.
      newTargetToken = undefined;
    }
  }

  if (!newTargetToken && newSourceToken) {
    newTargetChain = undefined;
  }

  return {
    source: { chain: newSourceChain, token: newSourceToken },
    target: { chain: newTargetChain, token: newTargetToken },
  };
};

// It takes chain and token for source and target and returns them.
// If provided data is valid, it returns the same.
// If data is not valid, it returns other available chain/token.
export const mapToValidPath = ({
  type,
  ...params
}: MapToValidPathParams): {
  source: { chain: Chain | undefined; token: Token | undefined };
  target: { chain: Chain | undefined; token: Token | undefined };
} => {
  if (type === "SWAP") {
    return mapToValidPathSwap(params);
  } else {
    return mapToValidPathBridge(params);
  }
};
