import React, { useEffect, useState } from 'react'
import FeatherIcon from 'feather-icons-react'
import classnames from 'classnames'
import { FormProvider, useForm, useFormContext, useWatch } from 'react-hook-form'
import ReactMarkdown from 'react-markdown'
import gfm from 'remark-gfm'
import { Dialog } from '@reach/dialog'
import { ethers } from 'ethers'
import { useTranslation } from 'react-i18next'
import { Card, ButtonLink, BlockExplorerLink, poolToast } from '@pooltogether/react-components'
import { useOnboard, useGovernanceChainId, useSendTransaction } from '@pooltogether/hooks'

import { ActionsCard } from 'lib/components/ProposalCreation/ActionsCard'
import { useUserCanCreateProposal } from 'lib/hooks/useUserCanCreateProposal'
import { Button } from '@pooltogether/react-components'
import { shorten } from 'lib/utils/shorten'
import { useTransaction } from 'lib/hooks/useTransaction'
import { CONTRACT_ADDRESSES, DEFAULT_TOKEN_PRECISION } from 'lib/constants'
import { TxStatus } from 'lib/components/TxStatus'
import { Banner } from 'lib/components/Banner'
import { useAllProposals } from 'lib/hooks/useAllProposals'
import { getEmptySolidityDataTypeValue } from 'lib/utils/getEmptySolidityDataTypeValue'
import { numberWithCommas } from 'lib/utils/numberWithCommas'
import { useGovernorAlpha } from 'lib/hooks/useGovernorAlpha'
import { arrayRegex, dataArrayRegex, fixedArrayRegex } from 'lib/utils/isValidSolidityData'
import GovernorAlphaABI from 'abis/GovernorAlphaABI'
import { useIsWalletOnProperNetwork } from 'lib/hooks/useIsWalletOnProperNetwork'
import Link from 'next/link'
import { TextInputGroup } from 'lib/components/TextInputGroup'

export const EMPTY_INPUT = {
  type: null,
  name: null,
  value: null
}

export const EMPTY_FN = { inputs: [], name: null, type: null }

export const EMPTY_CONTRACT = {
  address: null,
  name: null,
  abi: null,
  custom: null,
  fn: EMPTY_FN
}

export const EMPTY_ACTION = {
  contract: EMPTY_CONTRACT
}

export const ProposalCreationForm = () => {
  const { t } = useTranslation()
  const { refetch: refetchAllProposals } = useAllProposals()

  const { userCanCreateProposal } = useUserCanCreateProposal()
  const formMethods = useForm({
    mode: 'onSubmit',
    defaultValues: {
      title: '',
      description: '',
      actions: [EMPTY_ACTION]
    }
  })

  const [showSummary, setShowSummary] = useState(false)
  const [validFormData, setValidFormData] = useState()
  const [showModal, setShowModal] = useState(false)

  const chainId = useGovernanceChainId()
  const governanceAddress = CONTRACT_ADDRESSES[chainId]?.GovernorAlpha
  const [txId, setTxId] = useState(0)
  const sendTx = useSendTransaction(t, poolToast)
  const tx = useTransaction(txId)

  const onCancelled = () => setShowModal(false)

  const onSuccess = () => {
    refetchAllProposals()
  }

  const submitTransaction = async () => {
    const params = getProposeParamsFromForm(validFormData, t)
    if (!params) return

    const txId = await sendTx({
      name: t('propose'),
      contractAbi: GovernorAlphaABI,
      contractAddress: governanceAddress,
      method: 'propose',
      params,
      callbacks: {
        onCancelled,
        onSuccess
      }
    })
    setTxId(txId)
    setShowModal(true)
  }

  const onSubmit = async (data) => {
    window?.scrollTo(0, 0)
    setShowSummary(true)
    setValidFormData(data)
  }
  const onError = (data) => {
    const parsedErrorMessages = []

    if (data?.title?.message) parsedErrorMessages.push(data.title.message)
    if (data?.description?.message) parsedErrorMessages.push(data.description.message)

    if (data?.actions) {
      data?.actions.forEach((action) => {
        if (action?.contract?.address?.message) {
          parsedErrorMessages.push(action.contract.address.message)
        }

        if (action?.contract?.message) {
          parsedErrorMessages.push(action.contract.message)
        }

        if (action?.contract?.fn?.message) {
          parsedErrorMessages.push(action.contract.fn.message)
        }

        if (action?.contract?.fn?.payableAmount?.message) {
          parsedErrorMessages.push(action.contract.fn.payableAmount.message)
        }

        if (action?.contract?.fn?.values) {
          Object.keys(action.contract.fn.values).forEach((fnName) => {
            if (action.contract.fn.values[fnName]?.message) {
              parsedErrorMessages.push(action.contract.fn.values[fnName].message)
            }
          })
        }
      })
    }

    poolToast.error(parsedErrorMessages.join('. '))
  }

  const closeModal = () => {
    setShowModal(false)
    setShowSummary(false)
  }

  return (
    <>
      <ProposalTransactionModal
        tx={tx}
        isOpen={showModal}
        closeModal={closeModal}
        resetForm={formMethods.reset}
      />
      <FormProvider {...formMethods}>
        <form onSubmit={formMethods.handleSubmit(onSubmit, onError)}>
          <div className={classnames('flex flex-col', { hidden: showSummary })}>
            <ActionsCard />
            <TitleCard />
            <DescriptionCard />
            {!userCanCreateProposal && <ProposalCreationWarning />}
            <Button className='mb-16 w-full' disabled={!userCanCreateProposal} type='submit'>
              {t('previewProposal')}
            </Button>
          </div>

          {showSummary && (
            <ProposalSummary
              submitTransaction={submitTransaction}
              showForm={() => {
                setShowSummary(false)
                window.scrollTo(0, 0)
              }}
              getValues={formMethods.getValues}
              handleSubmit={formMethods.handleSubmit}
            />
          )}
        </form>
      </FormProvider>
    </>
  )
}

const TitleCard = (props) => {
  const { t } = useTranslation()
  const { register } = useFormContext()

  return (
    <Card className='mb-6'>
      <h4 className='mb-2'>{t('title')}</h4>
      <p className='mb-6'>{t('theTitleIsDescription')}</p>
      <TextInputGroup
        className='border-accent-3'
        bgClasses='bg-body'
        alignLeft
        placeholder={t('enterTheTitleOfYourProposal')}
        id='_proposalTitle'
        label={t('proposalTitle')}
        name='title'
        required={t('blankIsRequired', { blank: t('title') })}
        register={register}
      />
    </Card>
  )
}

const DescriptionCard = (props) => {
  const { t } = useTranslation()
  const { register, control } = useFormContext()
  const name = 'description'
  const text = useWatch({ control, name, defaultValue: '' })

  return (
    <Card className='mb-6'>
      <h4 className='mb-2'>{t('description')}</h4>
      <p className='mb-8'>{t('theDescriptionShouldPresentInFullDescription')}</p>
      <MarkdownInputArea name={name} text={text} register={register} />
    </Card>
  )
}

const MarkdownInputArea = (props) => {
  const { text, name, register, disabled } = props

  const { t } = useTranslation()

  const tabs = [
    {
      title: t('write'),
      view: (
        <TextArea
          placeholder={t('addTheProposalDetailsHere')}
          rows={15}
          disabled={disabled}
          name='description'
          required={t('blankIsRequired', { blank: t('description') })}
          register={register}
        />
      )
    },
    {
      title: t('preview'),
      view: <MarkdownPreview className='h-96' text={text} />
    }
  ]

  return <TabbedView tabs={tabs} />
}

const TextArea = (props) => {
  const { register, required, pattern, validate, classNames, ...textAreaProps } = props
  return (
    <textarea
      className={classnames('h-96 p-4 xs:p-8 bg-body text-inverse w-full resize-none', classNames)}
      ref={register({ required, pattern, validate })}
      {...textAreaProps}
    />
  )
}

const MarkdownPreview = (props) => {
  const { text, className } = props

  // Extra div with padding aligns the 'Write' tab content with the 'Preview' tab content exactly
  return (
    <div style={{ paddingTop: 0, paddingBottom: 6 }}>
      <ReactMarkdown
        plugins={[gfm]}
        className={classnames(
          'p-4 xs:p-8 description whitespace-pre-wrap break-word overflow-y-auto tracking-wider',
          className
        )}
        children={text}
      />
    </div>
  )
}

const TabbedView = (props) => {
  const { tabs, initialTabIndex } = props
  const [selectedTabIndex, setSelectedTabIndex] = useState(initialTabIndex)

  return (
    <>
      <div className='flex'>
        {tabs.map((tab, index) => (
          <Tab
            key={`${tab.title}-${index}-tab`}
            tab={tab.title}
            isSelected={selectedTabIndex === index}
            setTab={() => setSelectedTabIndex(index)}
          />
        ))}
      </div>
      <div className='bg-body border border-accent-3 rounded-b-lg'>
        {tabs.map((tab, index) => (
          <div
            key={`${tab.title}-${index}-view`}
            className={classnames({ hidden: selectedTabIndex !== index })}
          >
            {tab.view}
          </div>
        ))}
      </div>
    </>
  )
}

TabbedView.defaultProps = {
  initialTabIndex: 0
}

const Tab = (props) => {
  const { tab, setTab, isSelected } = props
  return (
    <div
      className={classnames('p-4 sm:px-8 sm:py-4 font-bold cursor-pointer', {
        'bg-body border-accent-3 border-t border-l border-r rounded-t-lg': isSelected
      })}
      onClick={(e) => {
        e.preventDefault()
        setTab()
      }}
    >
      {tab}
    </div>
  )
}

// TODO: It'd be awesome to be able to link to a filled out form. Just stuff everything into query params?
const ProposalSummary = (props) => {
  const { t } = useTranslation()
  const { showForm, getValues, submitTransaction } = props

  const isWalletOnProperNetwork = useIsWalletOnProperNetwork()
  const { actions, title, description } = getValues()
  const chainId = useGovernanceChainId()

  return (
    <>
      <h4 className='mb-8'>{t('proposalReview')}</h4>
      <Card className='mb-6'>
        <h5 className=''>{t('title')}:</h5>
        <h6 className='text-accent-1 p-4 xs:p-8'>{title}</h6>
        <h5 className=''>{t('description')}:</h5>
        <MarkdownPreview className='text-accent-1' text={description} />
        <h5 className='my-4'>{t('actions')}:</h5>
        {actions.map((action, index) => (
          <ActionSummary key={index} action={action} index={index} chainId={chainId} />
        ))}
      </Card>
      <Button
        className='w-full'
        secondary
        type='button'
        onClick={(e) => {
          e.preventDefault()
          showForm()
        }}
      >
        {t('editProposal')}
      </Button>
      <Button
        className='mt-4 mb-16 w-full'
        type='button'
        onClick={(e) => {
          e.preventDefault()
          submitTransaction()
        }}
        disabled={!isWalletOnProperNetwork}
      >
        {t('submitProposal')}
      </Button>
    </>
  )
}

const ActionSummary = (props) => {
  const { action, index, chainId } = props
  const { contract } = action
  const { name: contractName, address, fn } = contract
  const { inputs, name: fnName, values, payableAmount, payable } = fn

  const actionNumber = index + 1

  return (
    <div className='flex flex-col text-accent-1 mb-4 last:mb-0 break-all'>
      <span className='mb-2'>
        <b>{actionNumber}.</b>
        <span className='ml-2'>
          <b>{contractName}</b>
        </span>
        <BlockExplorerLink
          chainId={chainId}
          className='ml-2 text-inverse hover:text-accent-1'
          address={address}
        >
          (<span className='hidden sm:inline'>{address}</span>
          <span className='inline sm:hidden'>{shorten(address)}</span>)
        </BlockExplorerLink>
      </span>
      <span className='ml-4 xs:ml-8 mb-2'>
        <b>{fnName}</b>(
        {inputs.map(
          (input, index) => `${input.name}${inputs.length && index < inputs.length - 1 ? ', ' : ''}`
        )}
        )
      </span>
      {inputs.map((input, index) => (
        <div
          key={`${actionNumber}-${index}-fn-input`}
          className='ml-8 xs:ml-12 flex flex-col xs:flex-row mb-2'
        >
          <div className='xs:w-1/4 flex flex-wrap'>
            <b>{input.name}</b>
            <span className='mx-2'>{input.type}:</span>
          </div>
          <div className='xs:w-3/4'>
            <span className='text-inverse'>
              <FormattedInputValue {...input} chainId={chainId} value={values[input.name]} />
            </span>
          </div>
        </div>
      ))}
      {payable && (
        <div className='ml-8 xs:ml-12 mb-2 flex flex-col xs:flex-row '>
          {' '}
          <div className='xs:w-1/4 flex flex-wrap'>
            <b>payableAmount</b>
            <span className='mx-2'>ETH:</span>
          </div>
          <div className='xs:w-3/4'>
            <span className='text-inverse'>{payableAmount}</span>
          </div>
        </div>
      )}
    </div>
  )
}

const FormattedInputValue = (props) => {
  const { type, value, chainId } = props

  if (type === 'address') {
    return (
      <BlockExplorerLink
        chainId={chainId}
        className='text-inverse hover:text-accent-1'
        address={value}
      >
        <span className='hidden sm:inline'>{value || getEmptySolidityDataTypeValue(type)}</span>
        <span className='inline sm:hidden'>{shorten(value)}</span>
      </BlockExplorerLink>
    )
  }

  return value
}

const ProposalTransactionModal = (props) => {
  const { isOpen, closeModal, tx } = props

  const { t } = useTranslation()
  const { provider } = useOnboard()
  const [proposalId, setProposalId] = useState()

  const showClose = tx && (tx.error || tx.cancelled)
  const showNavigateToProposal = tx && !tx.error && tx.completed && proposalId !== undefined

  useEffect(() => {
    const getProposalId = async () => {
      const hash = tx.hash
      await tx.ethersTx.wait()
      const receipt = await provider.getTransactionReceipt(hash)
      const governorAlphaInterface = new ethers.utils.Interface(GovernorAlphaABI)
      const proposalCreatedEvent = receipt.logs.map((log) =>
        governorAlphaInterface.decodeEventLog('ProposalCreated', log.data)
      )[0]

      setProposalId(proposalCreatedEvent.id)
    }

    if (tx && tx.completed && !tx.error && !tx.cancelled) {
      getProposalId()
    }
  }, [tx, tx?.completed, tx?.error])

  const onClose = () => {
    if (tx && (tx.completed || tx.error || tx.cancelled)) {
      closeModal()
    }
  }

  return (
    <Dialog aria-label='Proposal Summary' isOpen={isOpen} onDismiss={onClose}>
      <Banner
        defaultBorderRadius={false}
        className='flex flex-col relative text-center rounded-b-lg sm:rounded-lg sm:max-w-3/4 mx-auto'
      >
        {showClose && (
          <button
            className='absolute right-4 top-4 close-button trans text-inverse hover:opacity-30'
            onClick={onClose}
          >
            <FeatherIcon icon='x' className='w-6 h-6' />
          </button>
        )}
        <TxStatus tx={tx} />
        {showNavigateToProposal && (
          <ButtonLink
            Link={Link}
            className='mt-8'
            href='/proposals/[id]'
            as={`/proposals/${proposalId}`}
          >
            {t('viewProposal')}
          </ButtonLink>
        )}
      </Banner>
    </Dialog>
  )
}

const ProposalCreationWarning = (props) => {
  const { t } = useTranslation()
  const { data: governorAlpha, isFetched } = useGovernorAlpha()

  if (!isFetched) return null

  const proposalThreshold = numberWithCommas(
    ethers.utils.formatUnits(governorAlpha.proposalThreshold, DEFAULT_TOKEN_PRECISION),
    { precision: 0 }
  )

  return (
    <div className='flex mb-7 mx-auto flex flex-col xs:flex-row text-center'>
      <FeatherIcon icon='alert-circle' className='w-6 h-6 text-red mx-auto xs:mr-4 mb-2 xs:mb-0' />
      <span>{t('inOrderToSubmitAProposalYouNeedDelegatedThreshold', { proposalThreshold })} </span>
    </div>
  )
}

/**
 * Values are the data that a user has input as a string.
 * When encoding the values from user input -> data for a tx, ethers is picky.
 * We need to parse the data from the strings in some cases.
 *
 *    solidity data type -> js data type before encoding
 *
 *    tuple -> object
 *    array of data types -> array of js data type
 *    numbers (int, unit, etc) -> string
 *    string, address (any hex) -> string
 *
 * @param {*} formData data values come in as strings
 * @param {*} t translate for errors
 */
const getProposeParamsFromForm = (formData, t) => {
  const targets = []
  const values = []
  const signatures = []
  const calldatas = []
  const description = `# ${formData.title}\n${formData.description}`

  try {
    formData.actions.forEach((action) => {
      targets.push(action.contract.address)

      if (action.contract.fn.payableAmount) {
        const ethAmount = action.contract.fn.payableAmount
        values.push(ethers.utils.parseEther(ethAmount).toString())
      } else {
        values.push(0)
      }

      const contractInterface = new ethers.utils.Interface(action.contract.abi)
      const fnFragment = contractInterface.fragments.find(
        (fragment) => fragment.name === action.contract.fn.name
      )

      signatures.push(fnFragment.format())

      const fnParameters = action.contract.fn.inputs.map((input) => {
        const rawData = action.contract.fn.values[input.name]

        if (!rawData) return getEmptySolidityDataTypeValue(input.type, input.components)

        if (
          input.type === 'tuple' ||
          dataArrayRegex.test(rawData) ||
          arrayRegex.test(rawData) ||
          fixedArrayRegex.test(rawData)
        ) {
          return JSON.parse(rawData, (key, value) =>
            typeof value === 'number' ? String(value) : value
          )
        }

        return rawData
      })
      const fullCalldata = contractInterface.encodeFunctionData(fnFragment, fnParameters)
      const calldata = fullCalldata.replace(contractInterface.getSighash(fnFragment), '0x')

      calldatas.push(calldata)
    })

    return [targets, values, signatures, calldatas, description]
  } catch (e) {
    console.warn(e.message)
    poolToast.error(t('errorEncodingData'))
    return null
  }
}