import {BrowserProvider, ethers} from "ethers";
import {useStore} from "@/store/store";
import {socket} from "@/socket.js";
import {contractOrNull, vaultAddress} from "@/blockchain/contract.js";
import {vaultAbi} from "@/blockchain/abi.js";
import {SingletonCoroutine, uuid} from "@/misc.js";
import {defineStore} from "pinia";
import {ref} from "vue";
import {metadataMap} from "@/version.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)

  // 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([])

  return {
    chainId, pendingOrders,
  }
})


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.  we store this value in walletStore.chainId, which may or may not be the same as
  // the application's useStore().chainId
  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 = []
      discoverVaults(addr)
      flushTransactions()
      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) {
    if (e.reason!=='rejected') {
      console.error(e, e.reason)
      throw e
    }
  }
}


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 addTestnet() {
  const info = {
    "chainId": "0x539",
    "chainName": "Dexorder Alpha Testnet",
    "rpcUrls": ["https://rpc.alpha.dexorder.trade"],
    "nativeCurrency": {
      "name": "Test Ethereum",
      "symbol": "TETH",
      "decimals": 18
    }
  };
  await window.ethereum.request({
    "method": "wallet_addEthereumChain",
    "params": [info]
  });
}


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, false)
async function _discoverVaults(owner) {
  const result = []
  const s = useStore()
  if( !owner || !s.chainId || !s.account) {
    s.vaults = []
    return
  }
  // todo multi-vault scan
  // console.log('_discoverVaults',owner)
  const num = 0
  const addr = vaultAddress(s.factory, s.vaultInitCodeHash, owner, num)
  // console.log(`vault ${num} at`, addr)
  if( addr === null ) {
    s.vaults = []
    return
  }
  console.log('provider', provider)
  if (!provider) {
    console.log('No provider')
    return  // do not change whatever was already found
  }
  const vault = new ethers.Contract(addr, vaultAbi, provider)
  let version = -1
  try {
    version = await vault.version();
    if( Number(version) === 1 ) {
      console.log(`found vault ${num} at ${addr}`)
      result.push(addr)
    }
    else
      console.error(`bad vault version ${version}`)
  }
  catch (e) {
    if( e.value==='0x' && e.code==='BAD_DATA' || e.revert===null && e.code==='CALL_EXCEPTION' )
      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?
  }
  if( s.account === owner ) { // double-check the account since it could have changed during our await
    s.vaults = result
    if( useWalletStore().pendingOrders.length ) {
      if (result.length)
        flushOrders(result[0])
      else
        ensureVault2(s.chainId, owner, 0)
    }
  }
}


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(s.vaults[num])
  else {
    console.log(`requesting vault ${owner} ${num}`)
    socket.emit('ensureVault', chainId, owner, num)
  }
}
// await sleep(5000) // prevent this process from running more than once every 5 seconds

const ensureVaultRoutine = new SingletonCoroutine(doEnsureVault, 100)


export const PendingOrderState = {
  Submitted: -100, // user clicked Place Order but the tx isn't sent to the wallet yet
  Signing: 0, // tx is sent to the wallet
  Rejected: -101, // user refused to sign the tx
  Sent: -102, // tx is awaiting blockchain mining
}


export async function pendOrder(order) {
  const s = useStore()
  const pend = {
    id: uuid(),
    chainId: s.chainId,
    placementTime: Date.now()/1000,
    vault: s.vaults.length ? s.vaults[0] : null,
    state: PendingOrderState.Submitted,
    order
  };
  useWalletStore().pendingOrders.splice(0,0, pend)
  console.log('pending order', pend.id, JSON.stringify(order))
  ensureVault()
}


export async function cancelOrder(vault, orderIndex) {
  console.log('cancel order', vault, orderIndex)
  pendTransaction(async (signer)=> {
    const contract = contractOrNull(vault, vaultAbi, 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) {
  pendTransaction(async (signer)=> {
    const contract = contractOrNull(vault, vaultAbi, signer)
    if( contract === null ) {
      console.error('vault contract was null while canceling order', vault)
      return null
    }
    return await contract.cancelAllDexorders()
  })
}

export function flushOrders(vault) {
  const ws = useWalletStore();
  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)
      pend.state = PendingOrderState.Signing
      needsFlush = true
    }
  }
  if (needsFlush)
    flushTransactions()
}


function pendOrderAsTransaction(pend) {
  pendTransaction(async (signer)=> {
    const contract = contractOrNull(pend.vault, vaultAbi, signer)
    if( contract === null ) {
      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')
        pend.state = PendingOrderState.Rejected
        return null
      }
      else {
        console.error('Unknown error while switching chain to pend order', e)
        return null
      }
    }
    console.log('placing order', pend.id)
    const tx = await contract.placeDexorder(pend.order) // todo update status
    pend.tx = tx
    pend.state = 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)
      pend.state = PendingOrderState.Rejected
      return true  // returning true means we handled the error.  any other return value will dump to console.
    }
  })
}

0
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()
  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)
    }
  })
}
