import React, { useEffect, useState } from "react";
import styled from "styled-components";

import Client, { CLIENT_EVENTS } from "@walletconnect/client";
import QRCodeModal from "@walletconnect/legacy-modal";
import { PairingTypes, SessionTypes } from "@walletconnect/types";
import { ERROR, getAppMetadata } from "@walletconnect/utils";
import * as encoding from "@walletconnect/encoding";
import { apiGetChainNamespace, ChainsMap } from "caip-api";
import { formatDirectSignDoc, stringifySignDocValues } from "cosmos-wallet";
import { BigNumber } from "ethers";

import Banner from "./components/Banner";
import Blockchain from "./components/Blockchain";
import Button from "./components/Button";
import Column from "./components/Column";
import Header from "./components/Header";
import Modal from "./components/Modal";
import Wrapper from "./components/Wrapper";
import {
  DEFAULT_APP_METADATA,
  DEFAULT_MAIN_CHAINS,
  DEFAULT_LOGGER,
  DEFAULT_EIP155_METHODS,
  DEFAULT_COSMOS_METHODS,
  DEFAULT_PROJECT_ID,
  DEFAULT_RELAY_URL,
  DEFAULT_TEST_CHAINS,
  DEFAULT_CHAINS,
} from "./constants";
import {
  apiGetAccountAssets,
  AccountAction,
  eip712,
  hashPersonalMessage,
  hashTypedDataMessage,
  verifySignature,
  AccountBalances,
  formatTestTransaction,
  ChainNamespaces,
  setInitialStateTestnet,
  getInitialStateTestnet,
} from "./helpers";
import { fonts } from "./styles";
import Toggle from "./components/Toggle";
import RequestModal from "./modals/RequestModal";
import PairingModal from "./modals/PairingModal";
import PingModal from "./modals/PingModal";

const SLayout = styled.div`
  position: relative;
  width: 100%;
  min-height: 100vh;
  text-align: center;
`;

const SContent = styled(Wrapper as any)`
  width: 100%;
  height: 100%;
  padding: 0 16px;
`;

const SLanding = styled(Column as any)`
  /* height: 600px; */
`;

const SButtonContainer = styled(Column as any)`
  width: 250px;
  margin: 50px 0;
`;

const SConnectButton = styled(Button as any)`
  border-radius: 8px;
  font-size: ${fonts.size.medium};
  height: 44px;
  width: 100%;
  margin: 12px 0;
`;

const SAccountsContainer = styled(SLanding as any)`
  height: 100%;
  padding-bottom: 30px;
  & h3 {
    padding-top: 30px;
  }
`;

const SToggleContainer = styled.div`
  width: 100%;
  display: flex;
  justify-content: center;
  align-items: center;
  margin: 10px auto;
  & > p {
    margin-right: 10px;
  }
`;

const SFullWidthContainer = styled.div`
  width: 100%;
  display: flex;
  justify-content: center;
  align-items: center;
  flex-wrap: wrap;
`;

const SAccounts = styled(SFullWidthContainer)`
  justify-content: space-between;
  & > div {
    margin: 12px 0;
    flex: 1 0 100%;
    @media (min-width: 648px) {
      flex: 0 1 48%;
    }
  }
`;

export default function App() {
  const [loading, setLoading] = useState(false);
  const [pending, setPending] = useState(false);
  const [fetching, setFetching] = useState(false);
  const [isTestnet, setIsTestnet] = useState(getInitialStateTestnet());

  const [modal, setModal] = useState("");
  const [client, setClient] = useState<Client>();
  const [uri, setUri] = useState("");
  const [session, setSession] = useState<SessionTypes.Created>();
  const [accounts, setAccounts] = useState<string[]>([]);
  const [pairings, setPairings] = useState<string[]>([]);
  const [result, setResult] = useState<{
    method: string;
    valid: boolean;
  } | null>();
  const [balances, setBalances] = useState<AccountBalances>({});
  const [chainData, setChainData] = useState<ChainNamespaces>({});
  const [chains, setChains] = useState<string[]>([]);

  const closeModal = () => setModal("");
  const openPairingModal = () => setModal("pairing");
  const openPingModal = () => setModal("ping");
  const openRequestModal = () => setModal("request");

  const init = async () => {
    setLoading(true);

    try {
      await loadChainData();

      const _client = await Client.init({
        logger: DEFAULT_LOGGER,
        relayUrl: DEFAULT_RELAY_URL,
        projectId: DEFAULT_PROJECT_ID,
      });
      setClient(_client);
      subscribeToEvents(_client);
      await checkPersistedState(_client);
    } catch (err) {
      throw err;
    } finally {
      setLoading(false);
    }
  };

  useEffect(() => {
    init();
  }, []);

  const subscribeToEvents = (_client: Client) => {
    if (typeof _client === "undefined") {
      throw new Error("WalletConnect is not initialized");
    }

    _client.on(CLIENT_EVENTS.pairing.proposal, async (proposal: PairingTypes.Proposal) => {
      const { uri } = proposal.signal.params;
      setUri(uri);
      console.log("EVENT", "QR Code Modal open");
      QRCodeModal.open(uri, () => {
        console.log("EVENT", "QR Code Modal closed");
      });
    });

    _client.on(CLIENT_EVENTS.pairing.created, async (proposal: PairingTypes.Settled) => {
      if (typeof client === "undefined") return;
      setPairings(client.pairing.topics);
    });

    _client.on(CLIENT_EVENTS.session.deleted, (deletedSession: SessionTypes.Settled) => {
      if (deletedSession.topic !== session?.topic) return;
      console.log("EVENT", "session_deleted");
      // TODO:
      // this.resetApp();
    });
  };

  const checkPersistedState = async (_client: Client) => {
    if (typeof _client === "undefined") {
      throw new Error("WalletConnect is not initialized");
    }
    // populates existing pairings to state
    setPairings(_client.pairing.topics);
    if (typeof session !== "undefined") return;
    // populates existing session to state (assume only the top one)
    if (_client.session.topics.length) {
      const session = await _client.session.get(_client.session.topics[0]);
      const chains = session.state.accounts.map(account =>
        account.split(":").slice(0, -1).join(":"),
      );
      setAccounts(session.state.accounts);
      setChains(chains);
      onSessionConnected(session);
    }
  };

  const connect = async (pairing?: { topic: string }) => {
    if (typeof client === "undefined") {
      throw new Error("WalletConnect is not initialized");
    }
    console.log("connect", pairing);
    if (modal === "pairing") {
      closeModal();
    }
    try {
      const supportedNamespaces: string[] = [];
      chains.forEach(chainId => {
        const [namespace] = chainId.split(":");
        if (!supportedNamespaces.includes(namespace)) {
          supportedNamespaces.push(namespace);
        }
      });
      const methods: string[] = supportedNamespaces
        .map(namespace => {
          switch (namespace) {
            case "eip155":
              return DEFAULT_EIP155_METHODS;
            case "cosmos":
              return DEFAULT_COSMOS_METHODS;
            default:
              throw new Error(`No default methods for namespace: ${namespace}`);
          }
        })
        .flat();
      const session = await client.connect({
        metadata: getAppMetadata() || DEFAULT_APP_METADATA,
        pairing,
        permissions: {
          blockchain: {
            chains,
          },
          jsonrpc: {
            methods,
          },
        },
      });

      onSessionConnected(session);
    } catch (e) {
      console.error(e);
      // ignore rejection
    }

    // close modal in case it was open
    QRCodeModal.close();
  };

  const disconnect = async () => {
    if (typeof client === "undefined") {
      throw new Error("WalletConnect is not initialized");
    }
    if (typeof session === "undefined") {
      throw new Error("Session is not connected");
    }
    await client.disconnect({
      topic: session.topic,
      reason: ERROR.USER_DISCONNECTED.format(),
    });
  };

  const onConnect = () => {
    if (typeof client === "undefined") {
      throw new Error("WalletConnect is not initialized");
    }
    if (client.pairing.topics.length) {
      return openPairingModal();
    }
    connect();
  };

  const onSessionConnected = async (incomingSession: SessionTypes.Settled) => {
    setSession(incomingSession);
    onSessionUpdate(incomingSession.state.accounts, incomingSession.permissions.blockchain.chains);
  };

  const onSessionUpdate = async (accounts: string[], chains: string[]) => {
    setChains(chains);
    setAccounts(accounts);
    await getAccountBalances();
  };

  const ping = async () => {
    if (typeof client === "undefined") {
      throw new Error("WalletConnect is not initialized");
    }
    if (typeof session === "undefined") {
      throw new Error("Session is not connected");
    }

    try {
      setPending(true);
      openPingModal();

      let valid = false;

      try {
        await client.session.ping(session.topic);
        valid = true;
      } catch (e) {
        valid = false;
      }

      // display result
      setResult({
        method: "ping",
        valid,
      });
    } catch (e) {
      console.error(e);
      setResult(null);
    } finally {
      setPending(false);
    }
  };

  const getAccountBalances = async () => {
    setFetching(true);
    try {
      const arr = await Promise.all(
        accounts.map(async account => {
          const [namespace, reference, address] = account.split(":");
          const chainId = `${namespace}:${reference}`;
          const assets = await apiGetAccountAssets(address, chainId);
          return { account, assets };
        }),
      );

      const balances: AccountBalances = {};
      arr.forEach(({ account, assets }) => {
        balances[account] = assets;
      });
      setBalances(balances);
    } catch (e) {
      console.error(e);
    } finally {
      setFetching(false);
    }
  };

  const getAllNamespaces = () => {
    const namespaces: string[] = [];
    DEFAULT_CHAINS.forEach(chainId => {
      const [namespace] = chainId.split(":");
      if (!namespaces.includes(namespace)) {
        namespaces.push(namespace);
      }
    });
    return namespaces;
  };

  const loadChainData = async () => {
    const namespaces = getAllNamespaces();
    const chainData: ChainNamespaces = {};
    await Promise.all(
      namespaces.map(async namespace => {
        let chains: ChainsMap | undefined;
        try {
          chains = await apiGetChainNamespace(namespace);
        } catch (e) {
          // ignore error
        }
        if (typeof chains !== "undefined") {
          chainData[namespace] = chains;
        }
      }),
    );
    setChainData(chainData);
  };

  const testSendTransaction = async (chainId: string) => {
    if (typeof client === "undefined") {
      throw new Error("WalletConnect is not initialized");
    }
    if (typeof session === "undefined") {
      throw new Error("Session is not connected");
    }

    try {
      // get ethereum address
      const account = accounts.find(account => account.startsWith(chainId));
      if (account === undefined) throw new Error("Account is not found");
      const address = account.split(":").pop();
      if (address === undefined) throw new Error("Address is invalid");

      // open modal
      openRequestModal();

      const tx = await formatTestTransaction(account);

      const balance = BigNumber.from(balances[account][0].balance || "0");
      if (balance.lt(BigNumber.from(tx.gasPrice).mul(tx.gasLimit))) {
        const formattedResult = {
          method: "eth_sendTransaction",
          address,
          valid: false,
          result: "Insufficient funds for intrinsic transaction cost",
        };
        setResult(formattedResult);
        return;
      }

      const result = await client.request({
        topic: session.topic,
        chainId,
        request: {
          method: "eth_sendTransaction",
          params: [tx],
        },
      });

      // format displayed result
      const formattedResult = {
        method: "eth_sendTransaction",
        address,
        valid: true,
        result,
      };

      // display result
      setResult(formattedResult);
    } catch (e) {
      console.error(e);
      setResult(null);
    } finally {
      setPending(false);
    }
  };

  const testSignPersonalMessage = async (chainId: string) => {
    if (typeof client === "undefined") {
      throw new Error("WalletConnect is not initialized");
    }
    if (typeof session === "undefined") {
      throw new Error("Session is not connected");
    }

    try {
      // test message
      const message = `My email is john@doe.com - ${Date.now()}`;

      // encode message (hex)
      const hexMsg = encoding.utf8ToHex(message, true);

      // get ethereum address
      const account = accounts.find(account => account.startsWith(chainId));
      if (account === undefined) throw new Error("Account is not found");
      const address = account.split(":").pop();
      if (address === undefined) throw new Error("Address is invalid");

      // personal_sign params
      const params = [hexMsg, address];

      // open modal
      openRequestModal();

      // send message
      const result = await client.request({
        topic: session.topic,
        chainId,
        request: {
          method: "personal_sign",
          params,
        },
      });

      //  split chainId
      const [namespace, reference] = chainId.split(":");

      const targetChainData = chainData[namespace][reference];

      if (typeof targetChainData === "undefined") {
        throw new Error(`Missing chain data for chainId: ${chainId}`);
      }

      const rpcUrl = targetChainData.rpc[0];

      // verify signature
      const hash = hashPersonalMessage(message);
      const valid = await verifySignature(address, result, hash, rpcUrl);

      // format displayed result
      const formattedResult = {
        method: "personal_sign",
        address,
        valid,
        result,
      };

      // display result
      setResult(formattedResult);
    } catch (e) {
      console.error(e);
      setResult(null);
    } finally {
      setPending(false);
    }
  };

  const testSignTypedData = async (chainId: string) => {
    if (typeof client === "undefined") {
      throw new Error("WalletConnect is not initialized");
    }
    if (typeof session === "undefined") {
      throw new Error("Session is not connected");
    }
    try {
      // test message
      const message = JSON.stringify(eip712.example);

      // get ethereum address
      const account = accounts.find(account => account.startsWith(chainId));
      if (account === undefined) throw new Error("Account is not found");
      const address = account.split(":").pop();
      if (address === undefined) throw new Error("Address is invalid");

      // eth_signTypedData params
      const params = [address, message];

      // open modal
      openRequestModal();

      // send message
      const result = await client.request({
        topic: session.topic,
        chainId,
        request: {
          method: "eth_signTypedData",
          params,
        },
      });

      //  split chainId
      const [namespace, reference] = chainId.split(":");

      const targetChainData = chainData[namespace][reference];

      if (typeof targetChainData === "undefined") {
        throw new Error(`Missing chain data for chainId: ${chainId}`);
      }

      const rpcUrl = targetChainData.rpc[0];

      // verify signature
      const hash = hashTypedDataMessage(message);
      const valid = await verifySignature(address, result, hash, rpcUrl);

      // format displayed result
      const formattedResult = {
        method: "eth_signTypedData",
        address,
        valid,
        result,
      };

      // display result
      setResult(formattedResult);
    } catch (e) {
      console.error(e);
      setResult(null);
    } finally {
      setPending(false);
    }
  };

  const testSignDirect = async (chainId: string) => {
    if (typeof client === "undefined") {
      throw new Error("WalletConnect is not initialized");
    }
    if (typeof session === "undefined") {
      throw new Error("Session is not connected");
    }

    try {
      // test direct sign doc inputs
      const inputs = {
        fee: [{ amount: "2000", denom: "ucosm" }],
        pubkey: "AgSEjOuOr991QlHCORRmdE5ahVKeyBrmtgoYepCpQGOW",
        gasLimit: 200000,
        accountNumber: 1,
        sequence: 1,
        bodyBytes:
          "0a90010a1c2f636f736d6f732e62616e6b2e763162657461312e4d736753656e6412700a2d636f736d6f7331706b707472653766646b6c366766727a6c65736a6a766878686c63337234676d6d6b38727336122d636f736d6f7331717970717870713971637273737a673270767871367273307a716733797963356c7a763778751a100a0575636f736d120731323334353637",
        authInfoBytes:
          "0a500a460a1f2f636f736d6f732e63727970746f2e736563703235366b312e5075624b657912230a21034f04181eeba35391b858633a765c4a0c189697b40d216354d50890d350c7029012040a020801180112130a0d0a0575636f736d12043230303010c09a0c",
      };

      // split chainId
      const [namespace, reference] = chainId.split(":");

      // format sign doc
      const signDoc = formatDirectSignDoc(
        inputs.fee,
        inputs.pubkey,
        inputs.gasLimit,
        inputs.accountNumber,
        inputs.sequence,
        inputs.bodyBytes,
        reference,
      );

      // get cosmos address
      const account = accounts.find(account => account.startsWith(chainId));
      if (account === undefined) throw new Error("Account is not found");
      const address = account.split(":").pop();
      if (address === undefined) throw new Error("Address is invalid");

      // cosmos_signDirect params
      const params = {
        signerAddress: address,
        signDoc: stringifySignDocValues(signDoc),
      };

      // open modal
      openRequestModal();

      // send message
      const result = await client.request({
        topic: session.topic,
        chainId,
        request: {
          method: "cosmos_signDirect",
          params,
        },
      });

      const targetChainData = chainData[namespace][reference];

      if (typeof targetChainData === "undefined") {
        throw new Error(`Missing chain data for chainId: ${chainId}`);
      }

      // TODO: check if valid
      const valid = true;

      // format displayed result
      const formattedResult = {
        method: "cosmos_signDirect",
        address,
        valid,
        result: result.signature.signature,
      };

      // display result
      setResult(formattedResult);
    } catch (e) {
      console.error(e);
      setResult(null);
    } finally {
      setPending(false);
    }
  };

  const testSignAmino = async (chainId: string) => {
    if (typeof client === "undefined") {
      throw new Error("WalletConnect is not initialized");
    }
    if (typeof session === "undefined") {
      throw new Error("Session is not connected");
    }

    try {
      // split chainId
      const [namespace, reference] = chainId.split(":");

      // test amino sign doc
      const signDoc = {
        msgs: [],
        fee: { amount: [], gas: "23" },
        chain_id: "foochain",
        memo: "hello, world",
        account_number: "7",
        sequence: "54",
      };

      // get cosmos address
      const account = accounts.find(account => account.startsWith(chainId));
      if (account === undefined) throw new Error("Account is not found");
      const address = account.split(":").pop();
      if (address === undefined) throw new Error("Address is invalid");

      // cosmos_signAmino params
      const params = { signerAddress: address, signDoc };

      // open modal
      openRequestModal();

      // send message
      const result = await client.request({
        topic: session.topic,
        chainId,
        request: {
          method: "cosmos_signAmino",
          params,
        },
      });

      const targetChainData = chainData[namespace][reference];

      if (typeof targetChainData === "undefined") {
        throw new Error(`Missing chain data for chainId: ${chainId}`);
      }

      // TODO: check if valid
      const valid = true;

      // format displayed result
      const formattedResult = {
        method: "cosmos_signAmino",
        address,
        valid,
        result: result.signature.signature,
      };

      // display result
      setResult(formattedResult);
    } catch (e) {
      console.error(e);
      setResult(null);
    } finally {
      setPending(false);
    }
  };

  const getEthereumActions = (): AccountAction[] => {
    return [
      { method: "eth_sendTransaction", callback: testSendTransaction },
      { method: "personal_sign", callback: testSignPersonalMessage },
      { method: "eth_signTypedData", callback: testSignTypedData },
    ];
  };

  const getCosmosActions = (): AccountAction[] => {
    return [
      { method: "cosmos_signDirect", callback: testSignDirect },
      { method: "cosmos_signAmino", callback: testSignAmino },
    ];
  };

  const getBlockchainActions = (chainId: string) => {
    const [namespace] = chainId.split(":");
    switch (namespace) {
      case "eip155":
        return getEthereumActions();
      case "cosmos":
        return getCosmosActions();
      default:
        break;
    }
  };

  const toggleTestnets = () => {
    const nextIsTestnetState = !isTestnet;
    setIsTestnet(nextIsTestnetState);
    // TODO: rename "setLocalStorage..."
    setInitialStateTestnet(nextIsTestnetState);
  };

  const handleChainSelectionClick = (chainId: string) => {
    if (chains.includes(chainId)) {
      setChains(chains.filter(chain => chain !== chainId));
    } else {
      setChains([...chains, chainId]);
    }
  };

  const renderModal = () => {
    switch (modal) {
      case "pairing":
        if (typeof client === "undefined") {
          throw new Error("WalletConnect is not initialized");
        }
        return <PairingModal pairings={client.pairing.values} connect={connect} />;
      case "request":
        return <RequestModal pending={pending} result={result} />;
      case "ping":
        return <PingModal pending={pending} result={result} />;
      default:
        return null;
    }
  };

  const renderContent = () => {
    const chainOptions = isTestnet ? DEFAULT_TEST_CHAINS : DEFAULT_MAIN_CHAINS;
    return !accounts.length && !Object.keys(balances).length ? (
      <SLanding center>
        <Banner />
        <h6>
          <span>{`Using v${process.env.REACT_APP_VERSION || "2.0.0-beta"}`}</span>
        </h6>
        <SButtonContainer>
          <h6>Select chains:</h6>
          <SToggleContainer>
            <p>Testnets Only?</p>
            <Toggle active={isTestnet} onClick={toggleTestnets} />
          </SToggleContainer>
          {chainOptions.map(chainId => (
            <Blockchain
              key={chainId}
              chainId={chainId}
              chainData={chainData}
              onClick={handleChainSelectionClick}
              active={chains.includes(chainId)}
            />
          ))}
          <SConnectButton left onClick={onConnect} fetching={fetching} disabled={!chains.length}>
            {"Connect"}
          </SConnectButton>
        </SButtonContainer>
      </SLanding>
    ) : (
      <SAccountsContainer>
        <h3>Accounts</h3>
        <SAccounts>
          {accounts.map(account => {
            const [namespace, reference, address] = account.split(":");
            const chainId = `${namespace}:${reference}`;
            return (
              <Blockchain
                key={account}
                active={true}
                chainData={chainData}
                fetching={fetching}
                address={address}
                chainId={chainId}
                balances={balances}
                // TODO:
                actions={getBlockchainActions(chainId)}
              />
            );
          })}
        </SAccounts>
      </SAccountsContainer>
    );
  };

  return (
    <SLayout>
      <Column maxWidth={1000} spanHeight>
        <Header ping={ping} disconnect={disconnect} session={session} />
        <SContent>{loading ? "Loading..." : renderContent()}</SContent>
      </Column>
      <Modal show={!!modal} closeModal={closeModal}>
        {renderModal()}
      </Modal>
    </SLayout>
  );
}
