import {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, sleep} from "@/misc.js";


export function onChainChanged(chainId) {
  chainId = Number(chainId)
  const store = useStore()
  if( chainId !== store.chainId ) {
    console.log('chain changed', chainId)
    store.chainId = chainId
    store.account = null
    const provider = new ethers.BrowserProvider(window.ethereum, chainId);
    store.provider = provider
    provider.listAccounts().then((accounts)=>changeAccounts(chainId, accounts.map((a)=>a.address)))
  }
}

function changeAccounts(chainId, accounts) {
  // console.log('changeAccounts', chainId, accounts)
  const store = useStore()
  if( accounts.length === 0 ) {
    console.log('account logged out')
    store.account = null
    store.vaults = []
    store.vaultBalances = {}
  }
  else {
    const addr = accounts[0]
    console.log('account logged in', addr)
    store.account = addr
    discoverVaults(addr)
    flushTransactions()
    socket.emit('address', chainId, addr)
  }
}

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

export function detectChain() {
  new ethers.BrowserProvider(window.ethereum).getNetwork().then((network)=>{
    const chainId = network.chainId
    onChainChanged(chainId)
    window.ethereum.on('chainChanged', onChainChanged);
    window.ethereum.on('accountsChanged', onAccountsChanged);
  })
}


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() {
  await new ethers.BrowserProvider(window.ethereum).getSigner();
}

let pendingOrders = []


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
  }
  const vault = new ethers.Contract(addr, vaultAbi, s.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)
  }
  if( s.account === owner ) { // double-check the account since it could have changed during our await
    s.vaults = result
    if( pendingOrders.length )
      if( result.length )
        flushOrders(result[0])
      else
        ensureVault2(s.chainId.value, owner, 0)
  }
}


export function ensureVault() {
  const s = useStore()
  const owner = s.account;
  console.log('ensureVault', s.chainId.value, owner)
  if( !owner )
    return
  ensureVault2(s.chainId.value, 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) {
  await _discoverVaults(owner)
  if( !useStore().vaults[num] ) {
    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, false)


export async function pendOrder(order) {
  console.log('order', JSON.stringify(order))
  const s = useStore()
  if (!s.vaults.length) {
    pendingOrders.push(order)
    ensureVault()
  }
  else {
    const vault = s.vaults[0];
    pendOrderAsTransaction(vault, order)
  }
}


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.cancelOrder(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.cancelAll()
  })
}

export function flushOrders(vault) {
  for( const order of pendingOrders )
    pendOrderAsTransaction(vault, order)
  pendingOrders = []
  flushTransactions()
}


function pendOrderAsTransaction(vault, order) {
  pendTransaction(async (signer)=> {
    const contract = contractOrNull(vault, vaultAbi, signer)
    if( contract === null ) {
      console.error('vault contract was null while sending order transaction', vault)
      return null
    }
    return await contract.placeOrder(order)
  })
}


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


let flushing = 0 // semaphore

export function flushTransactions() {
  flushing++
  if( flushing === 1 )
    // noinspection JSIgnoredPromiseFromCall
    asyncFlushTransactions()
}

export async function asyncFlushTransactions() {
  let counter
  do {
    counter = flushing
    await asyncFlushTransactions2()
  } while( flushing > counter)
  flushing = 0
}

export async function asyncFlushTransactions2() {
  // todo rework into flushTransactions()
  const s = useStore()
  if( s.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 s.provider.getSigner();
  } catch (e) {
    // {
    //     "code": -32002,
    //     "message": "Already processing eth_requestAccounts. Please wait."
    // }
    console.log('signer denied')
    return
  }
  for (const sender of senders)
    doSendTransaction(sender, signer)
}

function doSendTransaction(sender, signer) {
  const s = useStore();
  sender(signer).then((tx)=>{
    console.log('sent transaction', tx)
    s.removeTransactionSender(sender)
    tx.wait().then((tr)=>console.log('tx receipt',tr))
  }).catch((e)=>{
    if( e.info?.error?.code === 4001 ) {
      console.log(`user rejected transaction`)
      s.removeTransactionSender(sender)
    }
    else {
      if( e.reason && e.info )
        console.error('error sending transaction', e.reason, e.info)
      else
        console.error('error sending transaction', e)
      s.removeTransactionSender(sender)
      // todo retry?
    }
  })
}
