import {BrowserProvider, ethers} from "ethers";
import {useStore} from "@/store/store";
import {socket} from "@/socket.js";
import {errorSuggestsMissingVault, SingletonCoroutine} from "@/misc.js";
import {newContract, vaultAddress, vaultContract} from "@/blockchain/contract.js";
import {defineStore} from "pinia";
import {computed, ref} from "vue";
import {metadataMap, version} from "@/version.js";
import {CancelAllTransaction, TransactionState, TransactionType} from "@/blockchain/transaction.js";


export let provider = null


export const useWalletStore = defineStore('wallet', ()=>{
  // this is what the wallet is logged into.  it could be different than the application's store.chainId.
  const chainId = ref(0)

  // OLD Pending Order Format
  // {
  //   chainId: 31337,  // must never be null, even if no wallet plugin exists.  chosen by app, not wallet.
  //   placementTime: Date.now(),
  //   state: PendingOrderState.Submitted
  //   tx: null  // transaction ID
  //   vault: '0x...',  // or null if account not logged in yet
  //   order: {tokenIn:..., tokenOut:..., ...} // blockchain binary order object
  // }
  const pendingOrders = ref([])

  // NEW Format is a single Transaction class
  const _tx = ref(null)
  const transaction = computed({
    get() {return _tx.value},
    set(v) {
      _tx.value = v;
      if (v===null) {
        if (progressionInvoker!==null) {
          clearTimeout(progressionInvoker)
          progressionInvoker = null
        }
      }
      else {
        transactionProgressor.invoke();
        if (progressionInvoker===null)
          progressionInvoker = setInterval(()=>transactionProgressor.invoke(), 1000)
      }
    },
  })

  return {
    chainId, pendingOrders, transaction,
  }
})


export function onChainChanged(chainId) {
  console.log('onChainChanged', chainId)
  chainId = Number(chainId)
  const store = useStore()
  const ws = useWalletStore()
  if( chainId !== ws.chainId ) {
    console.log('wallet chain changed', chainId)
    ws.chainId = chainId
    if (chainId.toString() in metadataMap) {
      console.log('app chain changed', chainId)
      store.chainId = chainId
      store.account = null
      provider = new BrowserProvider(window.ethereum, chainId)
      updateAccounts(chainId, provider)
    }
    else {
      console.log('app chain NOT changed')
    }
  }
}


export function updateAccounts(chainId, provider) {
  provider.listAccounts().then((accounts) => changeAccounts(chainId, accounts.map((a) => a.address)))
}


function changeAccounts(chainId, accounts) {
  // this is a notification from the wallet that the user selected a different blockchain.  that chain may or may not
  // be supported.
  console.log('changeAccounts', chainId, accounts)
  const store = useStore()
  if (chainId === store.chainId && accounts.length) {
    const addr = accounts[0]
    if (addr !== store.account) {
      console.log('account logged in', addr)
      store.account = addr
      store.vaults = []
      // one of these two methods will call flushTransactions()
      if (useWalletStore().transaction!==null)
        ensureVault()
      else
        discoverVaults(addr)
      socket.emit('address', chainId, addr)
    }
  }
  else {
    console.log('account logged out')
    store.account = null
    store.vaults = []
  }
}

function onAccountsChanged(accounts) {
  console.log('onAccountsChanged', accounts)
  const store = useStore()
  const ws = useWalletStore()
  if (accounts.length === 0 || accounts[0] !== store.account)
    changeAccounts(ws.chainId, accounts);
}

export function detectChain() {
  try {
    window.ethereum.on('chainChanged', onChainChanged);
    window.ethereum.on('accountsChanged', onAccountsChanged);
  }
  catch (e) {
    console.log('Could not connect change hooks to wallet', e)
    return
  }
  new ethers.BrowserProvider(window.ethereum).getNetwork().then((network)=>{
    const chainId = network.chainId
    onChainChanged(chainId)
  })
}


const errorHandlingProxy = {
  get(target, prop, proxy) {
    const got = Reflect.get(target, prop, proxy);
    if( typeof got !== 'function' ) {
      return got
    }
    else {
      return async function (...args) {
        try {
          return await got.apply(target, args)
        }
        catch (x) {
          target._connected(false)
          target._enabled = false
          if( x.code === 'NETWORK_ERROR' ) {
            // todo available chain names
            // store.error('Wrong Blockchain', 'Your wallet is connected to a different blockchain. Please select Arbitrum in your wallet.') // todo hardcoded arb
            console.error('wallet chain error', x)
            // store.chainId = store.chainInfo[]
            throw x
          }
          else {
            console.error('wallet error')
            throw x
          }
        }
      }
    }
  }
}


export async function connectWallet(chainId) {
  console.log('connectWallet', chainId)
  try {
    await switchChain(chainId)
    console.log('getSigner')
    const p = new BrowserProvider(window.ethereum, chainId)
    await p.getSigner()
    await updateAccounts(chainId, p)
  }
  catch (e) {
    console.log('connectWallet error', e.reason, e)
    if (e.reason==='rejected') {
      const ws = useWalletStore();
      const tx = ws.transaction
      if (tx) {
        tx.state = TransactionState.Rejected
        ws.transaction = null
      }
    }
    else {
      console.error(e, e.reason)
      throw e
    }
  }
}


function discoverVaults(owner) {
  const s = useStore()
  console.log('discoverVaults', owner)
  if( owner === null )
    s.vaults = []
  else
    doDiscoverVaults.invoke(owner)
}

const doDiscoverVaults = new SingletonCoroutine(_discoverVaults, 50)

async function _discoverVaults(owner) {
  const result = []
  const versions = []
  const s = useStore()
  if( !owner || !s.chainId || !s.account) {
    s.vaults = []
    return
  }
  // todo multi-vault scan
  // console.log('_discoverVaults',owner)
  for (let num=0; num < 1; num++) {
    const num = 0
    const addr = vaultAddress(s.factory, s.vaultInitCodeHash, owner, num)
    // console.log(`vault ${num} at`, addr)
    if (addr === null)  // no more vaults
      break
    if (!provider) {
      console.log('No provider')
      return  // do not change whatever was already found
    }
    const vault = await vaultContract(addr, provider)
    try {
      const version = Number(await vault.version())
      console.log(`found vault #${num} v${version} at ${addr}`)
      result.push(addr)
      versions.push(version)
    } catch (e) {
      if (errorSuggestsMissingVault(e))
        console.log(`no vault ${num} at ${addr}`)
      else
        console.error(`discoverVaults failed`, e)
      return  // do not change what was already found todo is this correct?
    }
  }
  console.log('new account === owner?', s.account, owner)
  if( s.account === owner ) { // double-check the account since it could have changed during our await
    s.vaults = result
    s.vaultVersions = versions
    if( useWalletStore().transaction ) {
      const num = 0 // todo multiple vaults
      if (result.length)
        flushOrders(s.chainId, owner, num, result[0])
      else
        ensureVault2(s.chainId, owner, num)
    }
  }
}


export function ensureVault() {
  ensureVaultFunc.invoke()
}


const ensureVaultFunc = new SingletonCoroutine(ensureVault1,1)


async function ensureVault1() {
  const s = useStore()
  const owner = s.account;
  if (owner===null)
    await connectWallet(s.chainId)
  ensureVault2(s.chainId, owner, 0)
}

export function ensureVault2(chainId, owner, num) {
  console.log('ensureVault2', chainId, owner, num)
  if( !chainId ) {
    console.log('cannot create vault: no chain selected')
    return
  }
  if( !owner ) {
    console.log('cannot create vault: no account logged in')
    return
  }
  ensureVaultRoutine.invoke(chainId, owner, num)
}

async function doEnsureVault(chainId, owner, num) {
  const s = useStore();
  if (s.vaults.length <= num)
    await _discoverVaults(owner)
  if( s.vaults[num] )
    flushOrders(chainId, owner, num, s.vaults[num])
  else {
    console.log(`requesting vault ${owner} ${num}`)
    socket.emit('ensureVault', chainId, owner, num)
  }
}

const ensureVaultRoutine = new SingletonCoroutine(doEnsureVault, 100)


export async function cancelOrder(vault, orderIndex) {
  console.log('cancel order', vault, orderIndex)
  pendTransaction(async (signer)=> {
    const contract = await vaultContract(vault, signer)
    if( contract === null ) {
      console.error('vault contract was null while canceling order', vault, orderIndex)
      return null
    }
    return await contract.cancelDexorder(orderIndex)
  })
}

export async function cancelAll(vault) {
  new CancelAllTransaction(useStore().chainId, vault).submit()
}


async function progressTransactions() {
  const s = useStore()
  const ws = useWalletStore();
  if( ws.transaction===null )
    return
  if( s.account === null ) {
    let signer = null
    try {
      signer = await provider.getSigner()
    }
    catch (e) {
      console.log('signer error', e.code, e.info.error.code)
      if (e?.info?.error?.code === 4001) {
        console.log('signer rejected')
        signer = null
      }
      else
        throw e
    }
    if (signer === null) {
        console.log('setting tx state to rejected')
        ws.transaction.state = TransactionState.Rejected
        ws.transaction = null
        return
    }
  }
  if( s.vault === null ) {
    ensureVault()
    return
  }
  if( ws.transaction.type === TransactionType.PlaceOrder ) {
    flushOrders(s.chainId, s.account, 0, s.vault)
  }
}

const transactionProgressor = new SingletonCoroutine(progressTransactions, 10)

let progressionInvoker = null


export function flushOrders(chainId, owner, num, vault) {
  const ws = useWalletStore();
  console.log('flushOrders', chainId, owner, num, vault)
  if (ws.transaction!==null && ws.transaction.type === TransactionType.PlaceOrder && ws.transaction.state < TransactionState.Proposed)
    ws.transaction.propose(owner, vault)
  let needsFlush = false
  for( const pend of ws.pendingOrders ) {
    if (pend.vault === null)
      pend.vault = vault
    if (pend.state === PendingOrderState.Submitted) {
      console.log('flushing order', pend.id)
      pendOrderAsTransaction(pend)
      setPendState(pend, PendingOrderState.Signing)
      needsFlush = true
    }
  }
  if (needsFlush)
    flushTransactions()
}


function pendOrderAsTransaction(pend) {
  pendTransaction(async (signer)=> {
    // console.log('pendTransaction')
    let contract
    try {
      contract = await vaultContract(pend.vault, signer)
    }
    catch (e) {
      console.error('vault contract was null while sending order transaction', pend.vault)
      return null
    }
    try {
      await switchChain(pend.chainId)
    }
    catch (e) {
      if(e.code===4001) {
        console.log('user refused chain switch')
        setPendState(pend, PendingOrderState.Rejected)
        return null
      }
      else {
        console.error('Unknown error while switching chain to pend order', e)
        return null
      }
    }
    if (pend.fee === null) {
      const [orderFee, gasFee] = await placementFee(pend.vault, pend.order)
      pend.fee = orderFee + gasFee
    }
    console.log('placing order', pend.id, pend.fee, pend.order)
    const tx = await contract.placeDexorder(pend.order, {value:pend.fee})
    pend.tx = tx
    setPendState(pend, PendingOrderState.Sent)
    console.log(`order ${pend.id} sent transaction`, tx)
    tx.wait().then((txReceipt)=>{
      console.log('mined order', pend.id, txReceipt)
      pend.receipt = txReceipt
      const ws = useWalletStore();
      ws.pendingOrders = ws.pendingOrders.filter((p)=>p!==pend)  // remove pend since order was mined
    })
    return tx
  },
  (e) => {
    if( e.info?.error?.code === 4001 ) {
      console.log(`wallet refused order`, pend.id)
      setPendState(pend, PendingOrderState.Rejected)
      return true  // returning true means we handled the error.  any other return value will dump to console.
    }
  })
}


export function pendTransaction(sender, errHandler) {
  const s = useStore()
  s.transactionSenders.push([sender,errHandler])
  flushTransactions()
}


const flushTransactionsRoutine = new SingletonCoroutine(asyncFlushTransactions,1)

export function flushTransactions() {
  flushTransactionsRoutine.invoke()
}

async function asyncFlushTransactions() {
  const s = useStore()
  const ws = useWalletStore()
  console.log('flushTransactions', ws.transaction, s.vault)
  if (ws.transaction !== null) {
    if (s.vault === null) {
      await ensureVault()
      if (s.vault === null) {
        console.error('vault could not be created')
        const tx = ws.transaction
        if (tx) {
          tx.state = TransactionState.Error
          ws.transaction = null
        }
        return
      }
    }
  }
  if( provider === null ) {
    console.log('warning: asyncFlushOrders() cancelled due to null provider')
    return
  }
  const senders = s.transactionSenders
  if (!senders.length)
    return
  console.log(`flushing ${senders.length} transactions`)
  let signer
  try {
    signer = await provider.getSigner();
  } catch (e) {
    // {
    //     "code": -32002,
    //     "message": "Already processing eth_requestAccounts. Please wait."
    // }
    console.log('signer denied')
    return
  }
  for (const [sender,errHandler] of senders)
    doSendTransaction(sender, signer, errHandler)
  s.transactionSenders = []
}

function doSendTransaction(sender, signer, errHandler) {
  const s = useStore();
  s.removeTransactionSender(sender)
  sender(signer).then((tx)=>{
    if (tx!==null) {
      console.log('sent transaction', tx)
      tx.wait().then((tr)=>{
        console.log('tx receipt',tr)
      })
    }
  }).catch(async (e)=>{
    let dumpErr = true
    if (errHandler!==undefined)
      dumpErr = await errHandler(e) !== true
    if (dumpErr) {
      if( e.reason && e.info )
        console.error('error sending transaction', e.reason, e.info)
      else
        console.error('error sending transaction', e)
    }
  })
}


export async function detectUpgrade() {
  if (!provider) {
    console.log('no provider!')
    return null
  }
  const s = useStore()
  if (!s.vault) {
    console.log('no vault logged in')
    return null
  }

  const info = version.chainInfo[s.chainId]
  if (!info) {
    console.log(`couldn't get chainInfo for ${s.chainId}`)
    return null
  }
  try {
    console.log('factory', info.factory)
    const [factory, vault] = await Promise.all([
        newContract(info.factory, 'IVaultFactory', provider),
        newContract(s.vault, 'IVault', provider),
    ])
    const vaultImpl = await vault.implementation()
    const latestImpl = await factory.implementation()
    console.log('vaultImpl / latestImpl', vaultImpl, latestImpl)
    if ( vaultImpl !== latestImpl ) {
      s.upgrade = latestImpl
      const impl = await newContract(latestImpl, 'IVault', provider)
      const version = await impl.version()
      console.log(`found vault version ${version}`)
      return version
    }
  }
  catch (e) {
    console.log('ignorable error while querying for an upgrade', e)
  }
  return null
}


function upgradeSender(vault, impl) {
  return async function (signer) {
    const v = await vaultContract(vault, signer)
    v.upgrade(impl)
  }
}


function upgradeError(e) {
  console.error('error while upgrading vault', e)
}


export async function upgradeVault(vault, impl) {
  pendTransaction(upgradeSender(vault, impl), upgradeError)
}


const _chainInfos = {
  // Arbitrum One
  42161: {
    "chainId": "0xa4b1",
    "chainName": "Arbitrum One",
    "rpcUrls": ["https://arbitrum-mainnet.infura.io"],
    "blockExplorerUrls": ['https://explorer.arbitrum.io'],
    "nativeCurrency": {
      "name": "Ethereum",
      "symbol": "ETH",
      "decimals": 18
    }
  },
  1337: {
    "chainId": "0x539",
    "chainName": "Dexorder Alpha Testnet",
    "rpcUrls": ["https://rpc.alpha.dexorder.trade"],
    "nativeCurrency": {
      "name": "Test Ethereum",
      "symbol": "TETH",
      "decimals": 18
    }
  },
  31337: {
    "chainId": "0x7a69",
    "chainName": "Mockchain",
    "rpcUrls": ["http://localhost:8545"],
    "nativeCurrency": {
      "name": "Test Ethereum",
      "symbol": "TETH",
      "decimals": 18
    }
  },
}


export async function switchChain(chainId) {
  if (useWalletStore().chainId === chainId)
    return
  await window.ethereum.request({
    "method": "wallet_switchEthereumChain",
    "params": [{"chainId": '0x' + chainId.toString(16)}]
  })
}


export async function addNetwork(chainId) {
  const info = _chainInfos[chainId]
  if (!info) {
    console.log(`No info to add chain ${chainId}`)
    return
  }
  await window.ethereum.request({
    "method": "wallet_addEthereumChain",
    "params": [info]
  });
}

