const crypto = require('crypto')

const _ = require('lodash/fp')
const db = require('./db')
const { getOperatorId } = require('./operator')
const {
  getTermsConditions,
  setTermsConditions,
} = require('./new-config-manager')

const NEW_SETTINGS_LOADER_SCHEMA_VERSION = 2
const PASSWORD_FILLED = 'PASSWORD_FILLED'
const SECRET_FIELDS = [
  'bitgo.BTCWalletPassphrase',
  'bitgo.LTCWalletPassphrase',
  'bitgo.ZECWalletPassphrase',
  'bitgo.BCHWalletPassphrase',
  'bitgo.DASHWalletPassphrase',
  'bitstamp.secret',
  'itbit.clientSecret',
  'kraken.privateKey',
  'binanceus.privateKey',
  'cex.privateKey',
  'binance.privateKey',
  'twilio.authToken',
  'telnyx.apiKey',
  'vonage.apiSecret',
  'inforu.apiKey',
  'galoy.walletId',
  'galoy.apiSecret',
  'bitfinex.secret',
  'sumsub.apiToken',
  'sumsub.privateKey',
]

/*
 * JSON.stringify isn't necessarily deterministic so this function may compute
 * different hashes for the same object.
 */
const md5hash = text => crypto.createHash('MD5').update(text).digest('hex')

const addTermsHash = configs => {
  const terms = _.omit(['hash'], getTermsConditions(configs))
  return !terms?.text
    ? configs
    : _.flow(
        _.get('text'),
        md5hash,
        hash => _.set('hash', hash, terms),
        setTermsConditions,
        _.assign(configs),
      )(terms)
}

const notifyReload = (dbOrTx, operatorId) =>
  dbOrTx.none('NOTIFY $1:name, $2', ['reload', JSON.stringify({ operatorId })])

function saveAccounts(accounts) {
  if (!accounts) {
    return Promise.resolve()
  }

  const accountsSql = `UPDATE user_config SET data = $1, valid = TRUE, schema_version = $2 WHERE type = 'accounts';
  INSERT INTO user_config (type, data, valid, schema_version)
  SELECT 'accounts', $1, TRUE, $2 WHERE 'accounts' NOT IN (SELECT type FROM user_config)`

  return Promise.all([loadAccounts(), getOperatorId('middleware')]).then(
    ([currentAccounts, operatorId]) => {
      const newAccounts = _.merge(currentAccounts, accounts)

      // Only allow one wallet scoring active at a time
      if (accounts.elliptic?.enabled && newAccounts.scorechain) {
        newAccounts.scorechain.enabled = false
      }

      if (accounts.scorechain?.enabled && newAccounts.elliptic) {
        newAccounts.elliptic.enabled = false
      }

      return db
        .tx(t =>
          t
            .none(accountsSql, [
              { accounts: newAccounts },
              NEW_SETTINGS_LOADER_SCHEMA_VERSION,
            ])
            .then(() => notifyReload(t, operatorId)),
        )
        .catch(console.error)
    },
  )
}

function _loadAccounts(db, schemaVersion) {
  const sql = `SELECT data
    FROM user_config
    WHERE type = $1
      AND schema_version = $2
      AND valid
    ORDER BY id DESC
    LIMIT 1`

  return db.oneOrNone(
    sql,
    ['accounts', schemaVersion || NEW_SETTINGS_LOADER_SCHEMA_VERSION],
    row => row?.data?.accounts ?? {},
  )
}

const loadAccounts = schemaVersion => _loadAccounts(db, schemaVersion)

function hideSecretFields(accounts) {
  return _.flow(
    _.filter(path => !_.isEmpty(_.get(path, accounts))),
    _.reduce(
      (accounts, path) => _.assoc(path, PASSWORD_FILLED, accounts),
      accounts,
    ),
  )(SECRET_FIELDS)
}

function showAccounts(schemaVersion) {
  return loadAccounts(schemaVersion).then(hideSecretFields)
}

const insertConfigRow = (dbOrTx, data) =>
  dbOrTx.none(
    "INSERT INTO user_config (type, data, valid, schema_version) VALUES ('config', $1, TRUE, $2)",
    [data, NEW_SETTINGS_LOADER_SCHEMA_VERSION],
  )

function saveConfig(config) {
  return Promise.all([
    loadLatestConfigOrNone(),
    getOperatorId('middleware'),
  ]).then(([currentConfig, operatorId]) => {
    const newConfig = addTermsHash(_.assign(currentConfig, config))
    return db
      .tx(t =>
        insertConfigRow(t, { config: newConfig }).then(() =>
          notifyReload(t, operatorId),
        ),
      )
      .catch(console.error)
  })
}

function removeFromConfig(fields) {
  return Promise.all([
    loadLatestConfigOrNone(),
    getOperatorId('middleware'),
  ]).then(([currentConfig, operatorId]) => {
    const newConfig = _.omit(fields, currentConfig)
    return db
      .tx(t =>
        insertConfigRow(t, { config: newConfig }).then(() =>
          notifyReload(t, operatorId),
        ),
      )
      .catch(console.error)
  })
}

function migrationSaveConfig(config) {
  return loadLatestConfigOrNone().then(currentConfig => {
    const newConfig = _.assign(currentConfig, config)
    return insertConfigRow(db, { config: newConfig }).catch(console.error)
  })
}

const loadLatest = schemaVersion =>
  db
    .task(t =>
      Promise.all([
        loadLatestConfigOrNoneReturningVersion(t, schemaVersion),
        _loadAccounts(t, schemaVersion),
      ]),
    )
    .then(([configObj, accounts]) => ({
      config: configObj.config,
      accounts,
      version: configObj.version,
    }))

function loadLatestConfig() {
  const sql = `SELECT data
    FROM user_config
    WHERE type = 'config'
      AND schema_version = $1
      AND valid
    ORDER BY id DESC
    LIMIT 1`

  return db
    .oneOrNone(sql, [NEW_SETTINGS_LOADER_SCHEMA_VERSION])
    .then(row => (row ? row.data.config : {}))
    .catch(err => {
      throw err
    })
}

function loadLatestConfigOrNoneReturningVersion(db, schemaVersion) {
  const sql = `SELECT data, id
    FROM user_config
    WHERE type = 'config'
      AND schema_version = $1
      AND valid
    ORDER BY id DESC
    LIMIT 1`

  return db
    .oneOrNone(sql, [schemaVersion || NEW_SETTINGS_LOADER_SCHEMA_VERSION])
    .then(row => (row ? { config: row.data.config, version: row.id } : {}))
}

function loadLatestConfigOrNone(schemaVersion) {
  const sql = `SELECT data
    FROM user_config
    WHERE type = 'config'
      AND schema_version = $1
    ORDER BY id DESC
    LIMIT 1`

  return db
    .oneOrNone(sql, [schemaVersion || NEW_SETTINGS_LOADER_SCHEMA_VERSION])
    .then(row => (row ? row.data.config : {}))
}

function loadConfig(db, versionId) {
  const sql = `SELECT data
    FROM user_config
    WHERE id = $1
      AND type = 'config'
      AND schema_version = $2
      AND valid`

  return db
    .one(
      sql,
      [versionId, NEW_SETTINGS_LOADER_SCHEMA_VERSION],
      ({ data: { config } }) => config,
    )
    .catch(err => {
      if (err.name === 'QueryResultError') {
        throw new Error('No such config version: ' + versionId)
      }

      throw err
    })
}

function load(versionId) {
  if (!versionId) Promise.reject('versionId is required')

  return db
    .task(t => Promise.all([loadConfig(t, versionId), _loadAccounts(t)]))
    .then(([config, accounts]) => ({
      config,
      version: versionId,
      accounts,
    }))
}

const fetchCurrentConfigVersion = () => {
  const sql = `SELECT id FROM user_config
    WHERE type = 'config'
      AND valid
    ORDER BY id DESC
    LIMIT 1`
  return db.one(sql).then(row => row.id)
}

module.exports = {
  saveConfig,
  migrationSaveConfig,
  saveAccounts,
  loadAccounts,
  showAccounts,
  loadLatest,
  loadLatestConfig,
  loadLatestConfigOrNone,
  load,
  removeFromConfig,
  fetchCurrentConfigVersion,
}
