import { GqlAuthenticationError, GqlInputError } from '@/lib/error'
import { mapWalletResolveTypes } from '@/wallets/server/resolvers/util'
import { removeWalletProtocol, upsertWalletProtocol } from './protocol'
import { validateSchema, walletSettingsSchema } from '@/lib/validate'

const WalletOrTemplate = {
  __resolveType: walletOrTemplate => walletOrTemplate.__resolveType
}

const Wallet = {
  name: wallet => wallet.template.name,
  send: wallet => walletStatus(wallet, 'send'),
  receive: wallet => walletStatus(wallet, 'receive')
}

const WalletTemplate = {
  send: walletTemplate => walletTemplate.sendProtocols.length > 0 ? 'OK' : 'DISABLED',
  receive: walletTemplate => walletTemplate.recvProtocols.length > 0 ? 'OK' : 'DISABLED',
  protocols: walletTemplate => {
    return [
      ...walletTemplate.sendProtocols.map(protocol => ({
        id: `WalletTemplate-${walletTemplate.name}-${protocol}-send`,
        name: protocol,
        send: true
      })),
      ...walletTemplate.recvProtocols.map(protocol => ({
        id: `WalletTemplate-${walletTemplate.name}-${protocol}-recv`,
        name: protocol,
        send: false
      }))
    ]
  }
}

export const resolvers = {
  WalletOrTemplate,
  Wallet,
  WalletTemplate,
  Query: {
    wallets,
    wallet,
    walletSettings
  },
  Mutation: {
    updateWalletEncryption,
    updateKeyHash,
    resetWallets,
    setWalletPriorities,
    disablePassphraseExport,
    setWalletSettings
  }
}

async function wallets (parent, args, { me, models }) {
  if (!me) {
    throw new GqlAuthenticationError()
  }

  let wallets = await models.wallet.findMany({
    where: {
      userId: me.id
    },
    include: {
      template: true,
      protocols: true
    },
    orderBy: [
      { priority: 'asc' },
      { id: 'asc' }
    ]
  })

  let walletTemplates = await models.walletTemplate.findMany()

  wallets = wallets.map(mapWalletResolveTypes)
  walletTemplates = walletTemplates.map(t => {
    return {
      ...t,
      __resolveType: 'WalletTemplate'
    }
  })

  return [...wallets, ...walletTemplates]
}

async function wallet (parent, { id, name }, { me, models }) {
  if (!me) {
    throw new GqlAuthenticationError()
  }

  if (id) {
    const wallet = await models.wallet.findUnique({
      where: { id: Number(id), userId: me.id },
      include: {
        template: true,
        protocols: true
      }
    })
    if (!wallet) throw new GqlInputError('wallet not found')

    return mapWalletResolveTypes(wallet)
  }

  const template = await models.walletTemplate.findUnique({ where: { name } })
  return { ...template, __resolveType: 'WalletTemplate' }
}

function walletStatus (wallet, type) {
  const protocols = wallet.protocols.filter(protocol => type === 'send' ? protocol.send : !protocol.send)

  const disabled = protocols.every(protocol => !protocol.enabled)
  if (disabled) return 'DISABLED'

  const ok = protocols.every(protocol => protocol.status === 'OK')
  if (ok) return 'OK'

  const error = protocols.every(protocol => protocol.status === 'ERROR')
  if (error) return 'ERROR'

  return 'WARNING'
}

async function walletSettings (parent, args, { me, models }) {
  if (!me) throw new GqlAuthenticationError()

  return await models.user.findUnique({ where: { id: me.id } })
}

async function updateWalletEncryption (parent, { keyHash, wallets }, { me, models }) {
  if (!me) throw new GqlAuthenticationError()
  if (!keyHash) throw new GqlInputError('hash required')

  const { vaultKeyHash: oldKeyHash } = await models.user.findUnique({ where: { id: me.id } })

  return await models.$transaction(async tx => {
    for (const { id: walletId, protocols } of wallets) {
      for (const { name, send, config } of protocols) {
        const mutation = upsertWalletProtocol({ name, send })
        await mutation(parent, { walletId, ignoreKeyHash: true, ...config }, { me, models: tx, tx })
      }
    }

    // optimistic concurrency control:
    // make sure the user's vault key didn't change while we were updating the protocols
    await tx.user.update({
      where: { id: me.id, vaultKeyHash: oldKeyHash },
      data: {
        vaultKeyHash: keyHash,
        showPassphrase: false,
        vaultKeyHashUpdatedAt: new Date()
      }
    })

    return true
  })
}

async function updateKeyHash (parent, { keyHash }, { me, models }) {
  if (!me) throw new GqlAuthenticationError()

  const count = await models.$executeRaw`
    UPDATE users
    SET "vaultKeyHash" = ${keyHash}, "vaultKeyHashUpdatedAt" = NOW()
    WHERE id = ${me.id}
    AND "vaultKeyHash" = ''
  `

  return count > 0
}

async function resetWallets (parent, { newKeyHash }, { me, models }) {
  if (!me) throw new GqlAuthenticationError()

  const { vaultKeyHash: oldHash } = await models.user.findUnique({ where: { id: me.id } })

  await models.$transaction(async tx => {
    const protocols = await tx.walletProtocol.findMany({
      where: {
        send: true,
        wallet: {
          userId: me.id
        }
      }
    })

    for (const protocol of protocols) {
      await removeWalletProtocol(parent, { id: protocol.id }, { me, tx })
    }

    await tx.user.update({
      where: { id: me.id, vaultKeyHash: oldHash },
      // TODO(wallet-v2): nullable vaultKeyHash column
      data: {
        vaultKeyHash: newKeyHash,
        showPassphrase: true,
        vaultKeyHashUpdatedAt: new Date()
      }
    })
  })

  return true
}

async function disablePassphraseExport (parent, args, { me, models }) {
  if (!me) throw new GqlAuthenticationError()

  await models.user.update({ where: { id: me.id }, data: { showPassphrase: false } })

  return true
}

async function setWalletPriorities (parent, { priorities }, { me, models }) {
  if (!me) {
    throw new GqlAuthenticationError()
  }

  await models.$transaction(async tx => {
    for (const { id, priority } of priorities) {
      await tx.wallet.update({
        where: { userId: me.id, id: Number(id) },
        data: { priority }
      })
    }
  })

  return true
}

async function setWalletSettings (parent, { settings }, { me, models }) {
  if (!me) throw new GqlAuthenticationError()

  await validateSchema(walletSettingsSchema, settings)

  await models.user.update({ where: { id: me.id }, data: settings })

  return true
}
