import axios from 'axios'
import uuid from 'uuid/v4'
import PouchDb from 'pouchdb-browser'
import PouchDbAuthentication from 'pouchdb-authentication'
import bus, { SYNC } from '@/utils/bus-event'
import IUser from '@/models/IUser'
import notif from '@/utils/notif'
import { debounce } from 'lodash-es'
import { confirmation, toHex } from '@/utils'
import cryptoService from '@/services/CryptoService'
import IEncryptable from '@/models/IEncryptable'
import IEncrypted from '@/models/IEncrypted'
import IEncryptPassword from '@/models/IEncryptPassword'
import { SET_KEY_URL, GET_KEY_URL } from '@/utils/serverless-url'
import store from '@/store'

PouchDb.plugin(PouchDbAuthentication)

const emit = debounce((ev: string, arr?: string[]) => bus.$emit(ev, arr), 150)

const remoteConfig = {
  skip_setup: true,
  ajax: {
    cache: true,
    withCredentials: true
  }
}
const LOCALE_DB: string = 'LOCALE_DB'
const REMOTE: string = 'https://juliencalixte.ddns.net/database'

class CouchService {
  public remote: PouchDB.Database = new PouchDb(
    `${REMOTE}/vaquant`,
    remoteConfig
  )
  public db: PouchDB.Database | null = new PouchDb(LOCALE_DB)
  public remoteUserDb: PouchDB.Database | null = null
  public userDb: PouchDB.Database | null = null
  private user: IUser | null = null
  private sync: PouchDB.Replication.Sync<{}> | null = null
  private userSync: PouchDB.Replication.Sync<{}> | null = null
  private eventListened: boolean = false

  constructor() {
    if (!this.eventListened) {
      this.eventListened = true
      window.addEventListener('online', () => {
        if (!this.sync) {
          this.initLive()
        }
      })
      window.addEventListener('offline', () => {
        this.cancelSync()
      })
    }
  }

  public async initLive(): Promise<boolean> {
    if (!this.db) {
      return false
    }
    await this.cancelSync()
    if (this.userDb && this.remoteUserDb) {
      this.userSync = this.userDb
        .sync(this.remoteUserDb, {
          live: true,
          retry: true
        })
        .on('change', (result: PouchDB.Replication.SyncResult<any>) => {
          if (result.direction === 'pull') {
            emit(SYNC, result.change.docs.map((doc: any) => doc._id))
          }
        })
    }

    this.sync = this.db
      .sync(this.remote, {
        live: true,
        retry: true,
        filter: 'account/by_user',
        query_params: {
          userId: (this.user && this.user.userId) || '',
          accountIds: [...store.getters.publicAccountIds]
        }
      })
      .on('change', (result: PouchDB.Replication.SyncResult<any>) => {
        if (result.direction === 'pull') {
          const docNoCurrency = result.change.docs.find(
            (doc: any) => !doc._id.startsWith('cur')
          )
          if (docNoCurrency) {
            emit(SYNC)
            confirmation('Mise à jour...')
          }
        } else if (result.direction === 'push') {
          emit(SYNC)
        }
      })
      .on('error', (error: any) => {
        // tslint:disable-next-line:no-console
        console.warn('on error', { error })
        if (error.name !== 'unauthorized') {
          notif.error(`une erreur s'est produite`)
        }
      })
    return true
  }

  public async hasLocaleDoc(): Promise<boolean> {
    if (!this.db || this.db.name !== LOCALE_DB) {
      return false
    }
    const docs = await this.db.allDocs()
    return !!docs.total_rows
  }

  public setUser(
    user: IUser | null,
    hexUser: string | null,
    replicateLocale: boolean
  ): void {
    const newUser: boolean =
      !this.user || (!!user && this.user.userId !== user.userId)
    this.user = user
    if (user) {
      if (newUser) {
        if (replicateLocale && this.db) {
          const vaquantDb = new PouchDb(`vaquant-${user.userId}`)
          this.db.replicate.to(vaquantDb).on('complete', async () => {
            if (this.db) {
              await this.db.destroy()
              this.db = vaquantDb
            }
          })
        } else {
          this.db = new PouchDb(`vaquant-${user.userId}`)
        }
      }
      // this.remoteUserDb = hexUser
      //   ? new PouchDb(`${REMOTE}/userdb-${hexUser}`, remoteConfig)
      //   : null
      // this.userDb = hexUser ? new PouchDb(`userdb-${hexUser}`) : null
      this.initLive()
    } else {
      this.cancelSync()
    }
  }

  public get online(): boolean {
    return navigator.onLine
  }

  public async get(id: string): Promise<any | null> {
    if (!this.db) {
      return null
    }
    return await this.db.get(id)
  }

  public async getRemote(id: string): Promise<any | null> {
    if (!this.remote) {
      return null
    }
    return await this.remote.get(id)
  }

  public async getByPrefix<T>(
    prefix: string
  ): Promise<PouchDB.Core.AllDocsResponse<T>> {
    if (!this.db) {
      return {
        offset: 0,
        total_rows: 0,
        rows: []
      }
    }
    return (await this.db.allDocs({
      include_docs: true,
      startkey: prefix,
      endkey: `${prefix}\ufff0`
    })) as PouchDB.Core.AllDocsResponse<T>
  }

  public async query(query: string): Promise<any> {
    if (!this.db) {
      return []
    }
    const response = await this.db.query(query, {
      include_docs: true
    })
    return response.rows.map((row: any) => row.doc)
  }

  public async save(doc: any): Promise<PouchDB.Core.Response> {
    if (!this.db) {
      return {
        id: '',
        rev: '',
        ok: false
      }
    }
    const response = await this.db.put(doc)
    return response
  }

  public async multipleSave(
    docs: any[]
  ): Promise<Array<PouchDB.Core.Response | PouchDB.Core.Error>> {
    if (!this.db) {
      return []
    }
    const response = await this.db.bulkDocs(docs)
    return response
  }

  public async retrievePassword(id: string, user: IUser): Promise<boolean> {
    if (!this.userDb || !this.remoteUserDb) {
      return false
    }
    if (this.userDb.get(id)) {
      return true
    }
    const response = await axios.post(GET_KEY_URL, {
      accountId: id,
      username: user.userId,
      salt: user.salt
    })
    if (response.data) {
      const passwordDoc: IEncryptPassword = {
        _id: id,
        password: response.data
      }
      const putResponse = await this.userDb.put(passwordDoc)
      if (putResponse.ok) {
        await this.userDb.sync(this.remoteUserDb)
      }
      return putResponse.ok
    }
    return false
  }

  public async encrypt(
    id: string,
    doc: IEncryptable,
    savePassword: boolean = false,
    usernames: string[] = []
  ): Promise<IEncrypted | null> {
    try {
      if (!this.userDb) {
        return null
      }
      let passwordDoc: IEncryptPassword | null = null
      try {
        passwordDoc = (await this.userDb.get(id)) as IEncryptPassword
      } catch (error) {
        // tslint:disable-next-line:no-console
        console.warn('encrypt retrieve password', { error, savePassword })
      }
      if (!passwordDoc && savePassword) {
        try {
          passwordDoc = await cryptoService.createPassword()
          passwordDoc._id = id
          await this.userDb.put(passwordDoc)
          if (this.remoteUserDb) {
            this.userDb.sync(this.remoteUserDb).then(() => {
              if (!passwordDoc || !usernames.length || !this.user) {
                return
              }
              axios.post(SET_KEY_URL, {
                accountId: id,
                password: passwordDoc.password,
                usernames,
                username: this.user.userId,
                salt: this.user.salt
              })
            })
          }
        } catch (error) {
          // tslint:disable-next-line:no-console
          console.warn('encrypt and save password', { error, savePassword })
        }
      }
      if (!passwordDoc) {
        return doc
      }
      return await cryptoService.encrypt(doc, passwordDoc.password)
    } catch (error) {
      // tslint:disable-next-line:no-console
      console.warn('encrypt and save', { error })
      return doc
    }
  }

  public async decrypt(
    id: string,
    doc: IEncrypted
  ): Promise<IEncryptable | null> {
    try {
      if (!this.userDb) {
        return doc
      }
      const passwordDoc = (await this.userDb.get(id)) as IEncryptPassword
      if (!passwordDoc) {
        return doc
      }
      return await cryptoService.decrypt(doc, passwordDoc.password)
    } catch (error) {
      return doc
    }
  }

  public async remove(id: string): Promise<boolean> {
    if (!this.db) {
      return false
    }
    const doc = await this.db.get(id)
    if (doc) {
      const response = await this.save({
        ...doc,
        _deleted: true
      })
      if (this.userDb) {
        const userDb = this.userDb
        userDb
          .get(id)
          .then((password: any) => {
            if (password) {
              userDb.put({
                ...password,
                _deleted: true
              })
            }
          })
          .catch((error: any) => {
            // tslint:disable-next-line:no-console
            console.warn('remove password', { error })
          })
      }
      return response.ok
    }
    return false
  }

  public newId(prefix: string, withUuid: boolean = false): string {
    return `${prefix}${Date.now().toString()}${withUuid ? uuid() : ''}`
  }

  public async purgeDb(): Promise<void> {
    if (this.user) {
      if (this.db) {
        await this.db.destroy()
        this.db = null
      }
      if (this.userDb) {
        await this.userDb.destroy()
        this.userDb = null
      }
      const hexUser: string = toHex(this.user.userId)
      this.setUser(this.user, hexUser, false)
    }
  }

  private cancelSync(): any {
    if (this.sync) {
      this.sync.cancel()
      this.sync = null
    }
    if (this.userSync) {
      this.userSync.cancel()
      this.userSync = null
    }
  }
}

export default new CouchService()
