import {
  useCallback, useEffect, useState, useMemo, useRef,
} from 'react';
import { useSDK } from '@metamask/sdk-react';
import { Web3 } from 'web3';

import Button from 'react-bootstrap/Button';
import Tab from 'react-bootstrap/Tab';
import Tabs from 'react-bootstrap/Tabs';
import Container from 'react-bootstrap/Container';
import Row from 'react-bootstrap/Row';
import Col from 'react-bootstrap/Col';
import Stack from 'react-bootstrap/Stack';

import './App.css';

import useList from './utils/List';
import { getContractAddressByChainId } from './utils/contract-config';
import Web3Context from './contexts/Web3Context';
import DAOArtifact from '../contractArtifacts/DAO.json';
import Work1Artifact from '../contractArtifacts/Work1.json';
import OnboardingArtifact from '../contractArtifacts/Onboarding.json';
import WorkContract from './components/work-contracts/WorkContract';
import AddPostModal from './components/posts/AddPostModal';
import ViewPostModal from './components/posts/ViewPostModal';
import Post from './utils/Post';
import Proposals from './components/Proposals';
import getAddressName from './utils/get-address-name';

function App() {
  const {
    sdk, connected, provider, chainId, account, balance,
  } = useSDK();

  const DAORef = useRef();
  const workRef = useRef();
  const onboardingRef = useRef();
  const [DAO, setDAO] = useState();
  const [work1, setWork1] = useState();
  const [onboarding, setOnboarding] = useState();
  const [balanceEther, setBalanceEther] = useState();
  const [reputation, setReputation] = useState();
  const [totalReputation, setTotalReputation] = useState();
  const [posts, dispatchPost] = useList();
  const [validationPools, dispatchValidationPool] = useList();
  const [members, dispatchMember] = useList();

  const [showAddPost, setShowAddPost] = useState(false);
  const [showViewPost, setShowViewPost] = useState(false);
  const [viewPost, setViewPost] = useState({});

  const web3ProviderValue = useMemo(() => ({
    provider,
    DAO,
    work1,
    onboarding,
    reputation,
    setReputation,
    account,
    chainId,
    posts,
    DAORef,
    workRef,
    onboardingRef,
  }), [
    provider, DAO, work1, onboarding, reputation, setReputation, account, chainId, posts,
    DAORef, workRef, onboardingRef]);

  useEffect(() => {
    if (!provider || balance === undefined) return;
    const web3 = new Web3(provider);
    setBalanceEther(web3.utils.fromWei(balance, 'ether'));
  }, [provider, balance]);

  /* -------------------------------------------------------------------------------- */
  /* --------------------------- BEGIN FETCHERS ------------------------------------- */
  /* -------------------------------------------------------------------------------- */

  const fetchReputation = useCallback(async () => {
    setReputation(await DAORef.current.methods.balanceOf(account).call());
    setTotalReputation(await DAORef.current.methods.totalSupply().call());
  }, [DAORef, account]);

  const fetchPost = useCallback(async (postId) => {
    const post = await DAORef.current.methods.posts(postId).call();
    post.authors = await DAORef.current.methods.getPostAuthors(postId).call();
    dispatchPost({ type: 'updateById', item: post });
    return post;
  }, [DAORef, dispatchPost]);

  const fetchPostId = useCallback(async (postIndex) => {
    const postId = await DAORef.current.methods.postIds(postIndex).call();
    return postId;
  }, [DAORef]);

  const fetchPosts = useCallback(async () => {
    const count = await DAORef.current.methods.postCount().call();
    let promises = [];
    dispatchPost({ type: 'refresh' });
    for (let i = 0; i < count; i += 1) {
      promises.push(fetchPostId(i));
    }
    const postIds = await Promise.all(promises);
    promises = [];
    postIds.forEach((postId) => {
      promises.push(fetchPost(postId));
    });
  }, [DAORef, dispatchPost, fetchPost, fetchPostId]);

  const fetchValidationPool = useCallback(async (poolIndex) => {
    const getPoolStatus = (pool) => {
      if (pool.resolved) {
        return pool.outcome ? 'Accepted' : 'Rejected';
      }
      return pool.timeRemaining > 0 ? 'In Progress' : 'Ready to Evaluate';
    };
    const pool = await DAORef.current.methods.validationPools(poolIndex).call();
    pool.id = Number(pool.id);
    pool.timeRemaining = new Date(Number(pool.endTime) * 1000) - new Date();
    pool.status = getPoolStatus(pool);
    dispatchValidationPool({ type: 'update', item: pool });

    // When remaing time expires, we want to update the status for this pool
    if (pool.timeRemaining > 0) {
      setTimeout(() => {
        pool.timeRemaining = 0;
        pool.status = getPoolStatus(pool);
        dispatchValidationPool({ type: 'update', item: pool });
      }, pool.timeRemaining);
    }
  }, [DAORef, dispatchValidationPool]);

  const fetchValidationPools = useCallback(async () => {
    // TODO: Pagination
    // TODO: Memoization
    // TODO: Caching
    const count = await DAORef.current.methods.validationPoolCount().call();
    const promises = [];
    dispatchValidationPool({ type: 'refresh' });
    for (let i = 0; i < count; i += 1) {
      promises.push(fetchValidationPool(i));
    }
    await Promise.all(promises);
  }, [DAORef, dispatchValidationPool, fetchValidationPool]);

  const fetchMember = useCallback(async (memberIndex) => {
    const id = await DAORef.current.methods.members(memberIndex).call();
    const member = { id };
    member.reputation = await DAORef.current.methods.balanceOf(id).call();
    dispatchMember({ type: 'updateById', item: member });
    return member;
  }, [DAORef, dispatchMember]);

  const fetchMembers = useCallback(async () => {
    const count = await DAORef.current.methods.memberCount().call();
    const promises = [];
    dispatchMember({ type: 'refresh' });
    for (let i = 0; i < count; i += 1) {
      promises.push(fetchMember(i));
    }
    await Promise.all(promises);
  }, [DAORef, dispatchMember, fetchMember]);

  /* -------------------------------------------------------------------------------- */
  /* --------------------------- END FETCHERS --------------------------------------- */
  /* -------------------------------------------------------------------------------- */
  // In this effect, we initialize everything and add contract event listeners.
  useEffect(() => {
    if (!provider || !chainId || !account || balance === undefined) return () => {};
    const DAOAddress = getContractAddressByChainId(chainId, 'DAO');
    const Work1Address = getContractAddressByChainId(chainId, 'Work1');
    const OnboardingAddress = getContractAddressByChainId(chainId, 'Onboarding');
    const web3 = new Web3(provider);
    const DAOContract = new web3.eth.Contract(DAOArtifact.abi, DAOAddress);
    const Work1Contract = new web3.eth.Contract(Work1Artifact.abi, Work1Address);
    const OnboardingContract = new web3.eth.Contract(OnboardingArtifact.abi, OnboardingAddress);
    DAORef.current = DAOContract;
    workRef.current = Work1Contract;
    onboardingRef.current = OnboardingContract;

    fetchReputation();
    fetchMembers();
    fetchPosts();
    fetchValidationPools();

    setDAO(DAOContract);
    setWork1(Work1Contract);
    setOnboarding(OnboardingContract);

    // const fetchReputationInterval = setInterval(() => {
    //   // console.log('reputation', reputation);
    //   if (reputation !== undefined) {
    //     clearInterval(fetchReputationInterval);
    //     return;
    //   }
    //   fetchReputation();
    // }, 1000);

    /* -------------------------------------------------------------------------------- */
    /* --------------------------- BEGIN EVENT HANDLERS ------------------------------- */
    /* -------------------------------------------------------------------------------- */

    DAOContract.events.PostAdded({ fromBlock: 'latest' }).on('data', (event) => {
      console.log('event: post added');
      fetchPost(event.returnValues.id);
    });

    DAOContract.events.ValidationPoolInitiated({ fromBlock: 'latest' }).on('data', (event) => {
      console.log('event: validation pool initiated');
      fetchValidationPool(event.returnValues.poolIndex);
    });

    DAOContract.events.ValidationPoolResolved({ fromBlock: 'latest' }).on('data', (event) => {
      console.log('event: validation pool resolved');
      fetchReputation();
      fetchValidationPool(event.returnValues.poolIndex);
    });

    Work1Contract.events.AvailabilityStaked({ fromBlock: 'latest' }).on('data', () => {
      fetchReputation();
    });

    OnboardingContract.events.AvailabilityStaked({ fromBlock: 'latest' }).on('data', () => {
      fetchReputation();
    });

    return () => {
      DAOContract.events.PostAdded().off();
      DAOContract.events.ValidationPoolInitiated().off();
      DAOContract.events.ValidationPoolResolved().off();
      Work1Contract.events.AvailabilityStaked().off();
      OnboardingContract.events.AvailabilityStaked().off();
    };
  }, [provider, account, chainId, balance, dispatchValidationPool, dispatchPost, reputation,
    DAORef, workRef, onboardingRef,
    fetchPost, fetchPosts, fetchReputation, fetchValidationPool, fetchValidationPools, fetchMembers,
  ]);

  /* -------------------------------------------------------------------------------- */
  /* --------------------------- END MAIN INITIALIZION EFFECT ----------------------- */
  /* -------------------------------------------------------------------------------- */

  /* -------------------------------------------------------------------------------- */
  /* --------------------------- BEGIN UI ACTIONS ----------------------------------- */
  /* -------------------------------------------------------------------------------- */

  const connect = useCallback(async () => {
    try {
      await sdk?.connect();
    } catch (err) {
      console.warn('failed to connect..', err);
    }
  }, [sdk]);

  const disconnect = useCallback(async () => {
    try {
      sdk?.terminate();
    } catch (err) {
      console.warn('failed to disconnect..', err);
    }
  }, [sdk]);

  const watchReputationToken = useCallback(async () => {
    await provider.request({
      method: 'wallet_watchAsset',
      params: {
        type: 'ERC20',
        options: {
          address: getContractAddressByChainId(chainId, 'DAO'),
        },
      },
    });
  }, [provider, chainId]);

  const initiateValidationPool = useCallback(async (postId, poolDuration) => {
    const web3 = new Web3(provider);
    await DAO.methods.initiateValidationPool(
      postId,
      poolDuration ?? 3600,
      [1, 3],
      [1, 2],
      100,
      true,
      false,
      web3.eth.abi.encodeParameter('bytes', '0x00'),
    ).send({
      from: account,
      gas: 1000000,
      value: 10000,
    });
  }, [provider, DAO, account]);

  const stake = useCallback(async (poolIndex, amount, inFavor) => {
    console.log(`Attempting to stake ${amount} ${inFavor ? 'for' : 'against'} pool ${poolIndex}`);
    await DAO.methods.stakeOnValidationPool(poolIndex, amount, inFavor).send({
      from: account,
      gas: 999999,
    });

    // Since this is the result we expect from the server, we preemptively set it here.
    // We can let this value be negative -- this would just mean we'll be getting
    // at least one error from the server, and a corrected reputation.
    setReputation((current) => current - BigInt(amount));
  }, [DAO, account, setReputation]);

  const stakeHalfInFavor = useCallback(async (poolIndex) => {
    await stake(poolIndex, reputation / BigInt(2), true);
  }, [stake, reputation]);

  const evaluateOutcome = useCallback(async (poolIndex) => {
    await DAO.methods.evaluateOutcome(poolIndex).send({
      from: account,
      gas: 10000000,
    });
  }, [DAO, account]);

  const handleShowAddPost = () => setShowAddPost(true);

  const handleShowViewPost = useCallback(async ({ id }) => {
    const post = await Post.read(id);
    setViewPost(post);
    setShowViewPost(true);
  }, [setViewPost, setShowViewPost]);

  /* -------------------------------------------------------------------------------- */
  /* --------------------------- END UI ACTIONS ------------------------------------- */
  /* -------------------------------------------------------------------------------- */

  return (
    <Web3Context.Provider value={web3ProviderValue}>

      <AddPostModal show={showAddPost} setShow={setShowAddPost} postToBlockchain />

      <ViewPostModal show={showViewPost} setShow={setShowViewPost} post={viewPost} />

      {!connected && <Button onClick={() => connect()}>Connect</Button>}

      {connected && (
        <>
          <Container>
            <Row>
              <Col>
                {chainId !== '0xaa36a7' && (
                <div>
                  Please switch MetaMask to Sepolia testnet!
                </div>

                )}
              </Col>
            </Row>
            <Row>
              <Col>
                <Stack>
                  <div>
                    {chainId && `Chain ID: ${chainId}`}
                  </div>
                  <div>
                    {`Account: ${account}`}
                  </div>
                  <div>
                    {`Balance: ${balanceEther} ETH`}
                  </div>
                </Stack>
              </Col>
              <Col>
                <Stack>
                  <div>
                    {`Your REP: ${reputation?.toString()}`}
                  </div>
                  <div>
                    {`Total REP: ${totalReputation?.toString()}`}
                  </div>
                  <div>
                    <Button onClick={() => disconnect()}>Disconnect</Button>
                    <Button onClick={() => watchReputationToken()}>Watch REP in MetaMask</Button>
                  </div>
                </Stack>
              </Col>
            </Row>
          </Container>
          <Tabs>
            <Tab eventKey="admin" title="Admin">
              <h2>Members</h2>
              <div>
                {`Members count: ${members.length}`}
              </div>
              <div>
                <table className="table">
                  <thead>
                    <tr>
                      <th>ID</th>
                      <th>Reputation</th>
                    </tr>
                  </thead>
                  <tbody>
                    {members.filter((x) => !!x).map((member) => (
                      <tr key={member.id}>
                        <td>{member.id}</td>
                        <td>{member.reputation.toString()}</td>
                      </tr>
                    ))}
                  </tbody>
                </table>
              </div>
              {' '}
              <h2>Posts</h2>
              <div>
                <Button onClick={handleShowAddPost}>Add Post</Button>
              </div>
              <div>
                {`Posts count: ${posts.length}`}
              </div>
              <div>
                <table className="table">
                  <thead>
                    <tr>
                      <th>ID</th>
                      <th>Authors</th>
                      <th>Sender</th>
                      <th>Actions</th>
                    </tr>
                  </thead>
                  <tbody>
                    {posts.filter((x) => !!x).map((post) => (
                      <tr key={post.id}>
                        <td>{post.id.toString()}</td>
                        <td>
                          <Stack>
                            {post.authors.map(({ authorAddress, weightPercent }) => (
                              <div key={authorAddress}>
                                {getAddressName(chainId, authorAddress)}
                                {' '}
                                {weightPercent.toString()}
                                %
                              </div>
                            ))}
                          </Stack>
                        </td>
                        <td>{getAddressName(chainId, post.sender)}</td>
                        <td>
                          <Button onClick={() => handleShowViewPost(post)}>
                            View Post
                          </Button>
                          {' '}
                          Initiate Validation Pool
                          {' '}
                          <Button onClick={() => initiateValidationPool(post.id, 1)}>
                            1s
                          </Button>
                          {' '}
                          <Button onClick={() => initiateValidationPool(post.id, 20)}>
                            20s
                          </Button>
                          {' '}
                          <Button onClick={() => initiateValidationPool(post.id, 60)}>
                            60s
                          </Button>
                        </td>
                      </tr>
                    ))}
                  </tbody>
                </table>
              </div>
              <h2>Validation Pools</h2>
              <div>
                {`Validation Pool Count: ${validationPools.length}`}
              </div>
              <div>
                <table className="table">
                  <thead>
                    <tr>
                      <th>ID</th>
                      <th>Post ID</th>
                      <th>Sender</th>
                      <th>Fee</th>
                      <th>Binding</th>
                      <th>Quorum</th>
                      <th>WinRatio</th>
                      <th>
                        Redistribute
                        <br />
                        Losing Stakes
                      </th>
                      <th>Duration</th>
                      <th>End Time</th>
                      <th>
                        Stake
                        <br />
                        Count
                      </th>
                      <th>Status</th>
                      <th>Actions</th>
                    </tr>
                  </thead>
                  <tbody>
                    {validationPools.filter((x) => !!x).map((pool) => (
                      <tr key={pool.id}>
                        <td>{pool.id.toString()}</td>
                        <td>{pool.postId}</td>
                        <td>{getAddressName(chainId, pool.sender)}</td>
                        <td>{pool.fee.toString()}</td>
                        <td>
                          {pool.params.bindingPercent.toString()}
                          %
                        </td>
                        <td>{`${pool.params.quorum[0].toString()}/${pool.params.quorum[1].toString()}`}</td>
                        <td>{`${pool.params.winRatio[0].toString()}/${pool.params.winRatio[1].toString()}`}</td>
                        <td>{pool.params.redistributeLosingStakes.toString()}</td>
                        <td>{pool.params.duration.toString()}</td>
                        <td>{new Date(Number(pool.endTime) * 1000).toLocaleString()}</td>
                        <td>{pool.stakeCount.toString()}</td>
                        <td>{pool.status}</td>
                        <td>
                          {!pool.resolved && reputation > 0 && pool.timeRemaining > 0 && (
                          <>
                            <Button onClick={() => stakeHalfInFavor(pool.id)}>
                              Stake 1/2 REP
                            </Button>
                            {' '}
                            <Button onClick={() => stake(pool.id, reputation, true)}>
                              Stake All
                            </Button>
                            {' '}
                          </>
                          )}
                          {!pool.resolved && (pool.timeRemaining <= 0 || !reputation) && (
                          <Button onClick={() => evaluateOutcome(pool.id)}>
                            Evaluate Outcome
                          </Button>
                          )}
                        </td>
                      </tr>
                    ))}
                  </tbody>
                </table>
              </div>
            </Tab>
            <Tab eventKey="worker" title="Worker">
              {work1 && (
                <WorkContract
                  workContract={work1}
                  title="Work Contract 1"
                  verb="Work"
                  showProposePriceChange
                />
              )}
              {onboarding && (
                <WorkContract
                  workContract={onboarding}
                  title="Onboarding"
                  verb="Onboarding"
                  showRequestWork
                  showProposePriceChange
                />
              )}
            </Tab>
            <Tab eventKey="customer" title="Customer">
              {work1 && (
              <WorkContract
                workContract={work1}
                showAvailabilityActions={false}
                showAvailabilityAmount={false}
                onlyShowAvailable
                title="Work Contract 1"
                verb="Work"
                showRequestWork
              />
              )}
            </Tab>
            <Tab eventKey="proposals" title="Proposals">
              <Proposals />
            </Tab>
          </Tabs>
        </>
      )}
    </Web3Context.Provider>
  );
}

export default App;
