text: const Path = require('path')
  const Hoek = require('@hapi/hoek')
  const ObjectId = require('bson-objectid')
  const GpgPromised = require('gpg-promised')
  const Debug = require('debug')
  const debug = Debug('gpgfs.Bucket')
  
  
  const Utils = require('./utils')
  const GpgFsFile = require('./file')
  const IStorage = require('./interface-storage')
  
  /** Class representing gpgfs Bucket */
  class Bucket {
  
  
    /** @hideconstructor */
    constructor({id, name, root}){
      this.id = new ObjectId(id)
      this.name = name
      this.root = root
  
      this.opened = false
      this.index = null
      this.metadata = null
      this._fileCache = {}
      debug('new -', name || id)
  
      this.readKeychain = null
      this.metaKeychain = null
  
      this.keyFingerprints = {
        read: null,
        meta: null
      }
  
      this.keyPublics = {
        read: null,
        meta: null
      }
    }
  
    /**
     * Open and read metadata 
     * @method */
    async open(){
      if(this.opened){ return }
      this.index = null
      this.metadata = null
      this._fileCache = {}
      
      await Promise.all([
        this.getIndex(),
        this.getMetadata()
      ])
  
      /*this.readKeychain = null
      this.metaKeychain = null*/
  
      await this.loadReadKeys()
      
      this.name = this.metadata.bucketName
      this.opened = true
      debug('loaded ', this.name)
    }
  
    async release(){
      debug('releasing', this.id.toString())
  
      for(let id in this._fileCache){
        await this._fileCache[id].release()
  
        delete this._fileCache[id].bucket
        this._fileCache[id].bucket = null
        delete this._fileCache[id]
      }
  
      delete this.index
      delete this.metadata
      delete this._fileCache
  
      this.index = null
      this.metadata = null
      this._fileCache = {}
    }
  
    async releaseFile(file){
      delete this._fileCache[file.id.toString()]
      this._fileCache[file.id.toString()] = undefined
    }
  
    /**
     * Check if all metadata exists on disk
     * @method
     * @returns {boolean} */
    async exists(){
      const existance = await Promise.all([
        this.root.fileExists( this.indexPath ),
        this.root.fileExists( this.metadataPath )
      ])
      
      let e = existance[0] && existance[1]
      
      debug('exists', e)
      return e
    }
  
      /**
     * Create bucket if it does not exist 
     * @method
     */
    async create(){
      debug('create -', this.id)
      await this.root.cacheWhoami()
  
      //if(this.exists()){ throw new Error('bucket exists') }
      if(await this.root.fileExists( this.readKeyPath )){ throw new Error('bucket read key exists') }
      if(await this.root.fileExists( this.metaReadKeyPath )){ throw new Error('bucket meta read key exists') }
      if(await this.root.fileExists( this.indexPath )){ throw new Error('bucket index exists') }
      if(await this.root.fileExists( this.metadataPath )){ throw new Error('bucket metadata exists') }
  
      await this.root.touchDir(this.path)
      await this.root.touchDir(this.path + '/keys')
      await this.root.touchDir(this.path + '/objects')
      await this.root.touchDir(this.path + '/object-meta')
      await this.root.touchDir(this.path + '/object-lastchange')
  
      const nowTime = (new Date()).toISOString()
  
      const whoami = (await this.root.keychain.whoami())[0]
  
      //create keys
      debug('create bucket keys')
      await this.createReadKeys()
  
      //import and trust in root keychain
      debug('import bucket keys', this.keyFingerprints)
      const [[importedReadId], [importedMetaId]] = await Promise.all([
        this.root.keychain.importKey(this.keyPublics.read),
        this.root.keychain.importKey(this.keyPublics.meta)
      ])
  
      debug('trust keys', importedReadId, importedMetaId)
      await Promise.all([
        this.root.keychain.trustKey( importedReadId, '3' ),
        this.root.keychain.trustKey( importedMetaId, '3' )
      ])
  
      await this.setMetadata({
        owner: whoami,
        bucketId: {
          id: this.id.toHexString(),
          type: 'bucket_meta'
        },
        bucketKeys: {
          publics: this.keyPublics,
          fingerprints: this.keyFingerprints
        },
        created: nowTime,
        bucketName: this.name,
        cleartext: false,
        meta: [whoami, this.keyFingerprints.meta, this.keyFingerprints.read],
        readers: [whoami, this.keyFingerprints.read],
        writers: [whoami]
      })
  
      await this.setIndex({
        created: nowTime
      })
  
  
      this.readKeychain = null
      this.metaKeychain = null
    }
  
    /** 
     * Get a file instance
     * @method
     * @param {string} name File path
     * @returns {File}
     */
    async file(name){
      let file = await this.getFile(name)
  
      if(!file){
        file = new GpgFsFile({
          bucket: this,
          filePath: name
        })
      }
  
      return file
    }
  
    async getFileFromCache(id){
      return this._fileCache[id]
    }
  
    async getFile(path){
      await this.getIndex()
  
      if(!(this.index && this.index.objects)){
        return null
      }
  
      path = Path.join('/', path)
  
      let fileId = null
      for(const obj of this.index.objects){
        if(obj.path == path){
          fileId = obj.objectId.id
          break;
        }
      }
   
      if(!fileId){ return null }
  
      //! get file from cache
      let file = this._fileCache[fileId]
  
      if(!file){
        file = new GpgFsFile({
          bucket: this,
          id: fileId,
          filePath: path
        })
  
        await file.open()
        this._fileCache[file.id] = file
      }
  
      return file
    }
  
    get path(){ return '/buckets/bucket-'+this.id.toHexString() }
    get indexPath(){return this.path+'/index' }
    get metadataPath(){return this.path+'/metadata' }
    get readKeyPath(){return this.path+'/keys/read-key' }
    get metaReadKeyPath(){return this.path+'/keys/meta-read-key' }
  
    async getReciepents(){
      await this.root.cacheWhoami()
      let toList = [ this.root.whoami ]
  
      if(this.metadata){
  
        if(this.metadata.meta && this.metadata.meta.length > 0){
          toList = toList.concat(this.metadata.meta)
        }
  
        if(this.metadata.readers && this.metadata.readers.length > 0){
          toList = toList.concat(this.metadata.readers)
        }
  
        if(this.metadata.writers && this.metadata.writers.length > 0){
          toList = toList.concat(this.metadata.writers)
        }
  
      }
  
      return Utils.uniqueArray(toList)
    }
  
    async getMetaKeyReciepents(){
      let toList = [ Hoek.reach(this, 'metadata.owner', {default: this.root.whoami}) ]
  
      if(this.metadata){
  
        if(this.metadata.meta && this.metadata.meta.length > 0){
          toList = toList.concat(this.metadata.meta)
        }
  
        if(this.metadata.readers && this.metadata.readers.length > 0){
          toList = toList.concat(this.metadata.readers)
        }
  
        if(this.metadata.writers && this.metadata.writers.length > 0){
          toList = toList.concat(this.metadata.writers)
        }
      }
  
      return Utils.uniqueArray(toList)
    }
  
  
    async getReadKeyReciepents(){
      let toList = [ Hoek.reach(this, 'metadata.owner', {default: this.root.whoami}) ]
  
      if(this.metadata){
  
        if(this.metadata.readers && this.metadata.readers.length > 0){
          toList = toList.concat(this.metadata.readers)
        }
  
        if(this.metadata.writers && this.metadata.writers.length > 0){
          toList = toList.concat(this.metadata.writers)
        }
      }
  
      return Utils.uniqueArray(toList)
    }
  
    async getObjectIds(){
      const objectPaths = (await this.readDir('/objects'))
        .map(item=>{
          return item.replace('object-','')
        })
  
      debug('found ids', objectPaths)
      return objectPaths
    }
  
  
  
    
  
    async initReadKeys(){
      if(this.readKeychain !== null){ throw 'refuse to overwrite existing read key' }
      if(this.metaKeychain !== null){ throw 'refuse to overwrite existing metadata key' }
  
      this.readKeychain = new GpgPromised.KeyChain()
      this.metaKeychain = new GpgPromised.KeyChain()
  
      await Promise.all([
        await this.readKeychain.open(),
        await this.metaKeychain.open()
      ])
    }
  
    async createReadKeys(){
      await this.initReadKeys()
  
      await Promise.all([
        this.readKeychain.generateKey({
          //expire: '2y',
          unattend: true,
          name: `readers ${this.id}`,
          email: `bucket-readers-${this.id}@gpgfs.xyz`
        }),
        this.metaKeychain.generateKey({
          //expire: '2y',
          unattend: true,
          name: `metareaders ${this.id}`,
          email: `bucket-metareaders-${this.id}@gpgfs.xyz`
        })
      ])
  
      this.keyFingerprints.read = (await this.readKeychain.listSecretKeys())[0].fpr.user_id
      this.keyFingerprints.meta = (await this.metaKeychain.listSecretKeys())[0].fpr.user_id
  
      this.keyPublics.read = await this.readKeychain.exportPublicKey(`bucket-readers-${this.id}@gpgfs.xyz`)
      this.keyPublics.meta = await this.metaKeychain.exportPublicKey(`bucket-metareaders-${this.id}@gpgfs.xyz`)
  
      await this.saveReadKeys()
    }
  
    async saveReadKeys(){
      //! store read & metadata keys
      debug('saving bucket keys')
      const saveSecretText = async (keychain, path, to) =>{
        const who = await keychain.whoami()
        const key = await keychain.exportSecretKey(who[0])
        await this.root.writeFile(
          path,
          key,
          { to, encrypt: true }
        )
      }
  
      await Promise.all([
        saveSecretText(
          this.readKeychain,
          this.readKeyPath,
          await this.getReadKeyReciepents()
        ),
        saveSecretText(
          this.metaKeychain,
          this.metaReadKeyPath,
          await this.getMetaKeyReciepents()
        )
      ])
    }
  
    async loadReadKeys(){
      debug('loading bucket keys')
      await this.initReadKeys()
  
      let fromList = [ Hoek.reach(this, 'metadata.owner', {default: this.root.whoami}) ]
  
      const loadSecretKey = async (keychain, path) =>{
        const key = await this.root.readFile(path, true, null, null, {
          trust: 'direct',
          from: fromList
        })
  
        const [keyId] = await keychain.importKey(key)
  
        await keychain.trustKey(keyId, '5')
  
        const lookups = (this.metadata.writers||[]).map(async writer => {
          debug('\twriter\t',writer)
  
          const k = await keychain.lookupKey(writer)
          debug(k)
          await keychain.recvKey( k.keyid )
          await keychain.signKey(writer)
        })
  
        await Promise.all(lookups)
  
        return await keychain.whoami()
      }
  
      const whose = await Promise.all([
        loadSecretKey( this.readKeychain, this.readKeyPath ),
        loadSecretKey( this.metaKeychain, this.metaReadKeyPath )
      ])
  
      this.keyFingerprints.read = (await this.readKeychain.listSecretKeys())[0].fpr.user_id
      this.keyPublics.read = await this.readKeychain.exportPublicKey(whose[0][0])
  
  
      this.keyFingerprints.meta = (await this.metaKeychain.listSecretKeys())[0].fpr.user_id
      this.keyPublics.meta = await this.metaKeychain.exportPublicKey(whose[0][1])
  
      // Assert loaded key fingerprints and publics match listed in project json
      // import & trust read keys if not already in key ring
  
      //import and trust in root keychain
      debug('import bucket keys', this.keyFingerprints)
      const [[importedReadId], [importedMetaId]] = await Promise.all([
        this.root.keychain.importKey(this.keyPublics.read),
        this.root.keychain.importKey(this.keyPublics.meta)
      ])
  
      debug('trust keys', importedReadId, importedMetaId)
      await Promise.all([
        this.root.keychain.trustKey( importedReadId, '3' ),
        this.root.keychain.trustKey( importedMetaId, '3' )
      ])
    
    }
  
    async unloadReadKeys(){
      //! @todo
      throw 'not implemented'
    }
  
  
    /**
     * Bucket metadata 
     * @method 
     * @returns {gpgfs_model.bucket_meta} See [`gpgfs_model.bucket_meta`]{@link https://github.com/datapartyjs/gpgfs-model/blob/master/src/types/bucket_meta.js}
     */
    async getMetadata(){
      this.metadata = await this.root.readFile( this.metadataPath, true, 'bucket_meta', null, {from: Hoek.reach(this, 'metadata.owner')})
      return this.metadata
    }
  
    async setMetadata(value={}){
      const nowTime = (new Date()).toISOString()
      let newMetadata = Object.assign({lastchanged: nowTime}, this.metadata, value)
  
      await this.root.writeFile( this.metadataPath,
        newMetadata,
        {
          model: 'bucket_meta',
          encrypt: true,
          trust: 'direct',
          to: await this.getReciepents()
        }
      )
  
      if(!this.metadata){
        debug('creating metadata')
        this.metadata = newMetadata
      }
      else {
        debug('replacing metadata')
        this.metadata = newMetadata
      }
  
    }
  
      /**
     * Bucket index 
     * @method 
     * @returns {gpgfs_model.bucket_index} See [`gpgfs_model.bucket_index`]{@link https://github.com/datapartyjs/gpgfs-model/blob/master/src/types/bucket_index.js}
     */
    async getIndex(){
      if(this.index !== null){ return this.index }
      this.index = await this.root.readFile( this.indexPath, true, 'bucket_index', null, {from: Hoek.reach(this, 'metadata.writers')})
      return this.index   
    }
  
    async setIndex(value){
      const nowTime = (new Date()).toISOString()
      let newIndex = Object.assign({
        lastchanged: nowTime,
        bucketId: {
          id: this.id.toHexString(),
          type: 'bucket_meta'
        },
        objects: [],
        dirs: []
      }, value)
  
  
      await this.root.writeFile( this.indexPath,
        newIndex,
        {
          model: 'bucket_index',
          encrypt: true,
          trust: 'direct',
          to: await this.getReciepents()
        }
      )
  
      if(!this.index){
        debug('creating index')
        this.index = newIndex
      }
      else {
        debug('replacing index')
        this.index = newIndex
      }
    }
  
    async mkdir(path){
      debug('mkdir path -', path)
      await this.getIndex()
  
      path = Path.normalize( Path.join('/', path ))
  
      debug('\t', 'normalize -', path)
  
      if(this.index.dirs.indexOf(path) < 0){
        this.index.dirs.push(path)
        await this.setIndex({...this.index})
      }
    }
  
    async rmdir(path){
      debug('rmdir path -', path)
      await this.getIndex()
  
      path = Path.normalize( Path.join('/', path ))
  
      debug('\t', 'normalize -', path)
  
      const idx = this.index.dirs.indexOf(path)
      if(idx > -1){
        this.index.dirs.splice(idx, 1)
        await this.setIndex({...this.index})
      }
    }
  
    readDir(path){
      // Given a path, determine the objects and dirs local to that path
  
      debug('readDir ', path)
  
      const bucketPath = Path.normalize( Path.join('/', path) )
  
      debug('readDir ', this.name, bucketPath)
  
      const results = {
        files: {},
        dirs: []
      }
  
      //! Search objects
      for(const obj of this.index.objects){
  
        //debug('check file ', obj.path, ' startsWith(', bucketPath, ')')
  
        if(obj.path.startsWith(bucketPath)){
  
          const filePathToks = obj.path.replace(bucketPath, '').split('/')
          if(filePathToks[0] == ''){ filePathToks.shift() }
          const localPath = filePathToks[0]
  
          if(filePathToks.length > 1){
            //! add intermediate directory to list
            results.dirs.push( localPath )
          } else {
            results.files[ localPath ] = obj
          }
  
        }
      }
  
      for(let i = 0; i < Hoek.reach(this, 'index.dirs.length', {default: 0}); i++){
        
        const dirPath = this.index.dirs[ i ]
        if(dirPath.startsWith(bucketPath)){
  
          const filePathToks = dirPath.replace(bucketPath, '').split('/')
          if(filePathToks[0] == ''){ filePathToks.shift() }
          const localPath = filePathToks[0]
          results.dirs.push( localPath )
        }
      }
  
      results.dirs = Utils.uniqueArray( results.dirs )
  
      return results
    }
  
    /** @method
     * @param {File} file
     */
    async indexFile(file){
      let indexes = []
  
      await this.getIndex()
  
  
      for(const idx in Hoek.reach(this, 'index.objects')){
        
        let obj = this.index.objects[ idx ]
        if(obj.path == file.filePath || obj.objectId.id == file.id){
          indexes.push(idx)
        }
      }
  
      debug('found ', indexes.length, ' index entries matching path =', file.filePath)
  
      if(indexes.length > 1){ throw new Error('duplicate file path in index') }
  
      const idx = indexes[0]
      let oldIndex =  Hoek.reach(this, 'index.objects.'+idx)
      let newIndex = {
        created: file.metadata.created,
        objectId: file.metadata.objectId,
        path: file.metadata.path,
        size: file.lastchange.size,
        lastchanged: file.lastchange.lastchanged
      }
  
      debug('newIndex', newIndex)
  
      const parents = Utils.parentPaths( newIndex.path )
      
      if(parents && parents.length > 0){
        debug('creating parent dirs', parents)
        const mkParentDirs = parents.map(this.mkdir.bind(this))
  
        await Promise.all( mkParentDirs )
      }
  
      if(!oldIndex){
  
        await this.setIndex({
          ...this.index,
          objects: [].concat(this.index.objects, [newIndex])
        })
      }
      else {
  
        this.index.objects[ idx ] = newIndex
        await this.setIndex({...this.index })
      }
    }
  
    async unindexFile(file){
      debug('unindex')
      let indexes = []
  
      await this.getIndex()
  
  
      for(const idx in Hoek.reach(this, 'index.objects')){
        
        let obj = this.index.objects[ idx ]
        if(obj.path == file.filePath || obj.objectId.id == file.id){
          indexes.push(idx)
        }
      }
  
      debug('found ', indexes.length, ' index entries matching path =', file.filePath)
  
      if(indexes.length > 1){ throw new Error('duplicate file path in index') }
  
      const idx = indexes[0]
      let oldIndex =  Hoek.reach(this, 'index.objects.'+idx)
  
      if(oldIndex){
        debug('removing from index', oldIndex)
        this.index.objects.splice( idx, 1 );
  
        await this.setIndex({...this.index })
      }
    }
  
    async isWriter(id=null){
      //! @todo
      return true
    }
  
    async isOwner(id=null){
      //! @todo
      return true
    }
  
    async isReader(id=null){
      //! @todo
      return true
    }
  
    async isWritable({path, file}={}){
      const writer = await this.isWriter()
      const fsWritable = this.root.storage.mode == IStorage.MODE_WRITE
  
      return writer && fsWritable 
    }
  
    /**
     * Allow access for list of actors to type level of access
     * @param {Object} options
     * @param {('meta'|'readers'|'writers')} options.type Access type
     * @param {string[]} options.list List of emails or fingerprints
     */
    async addActor({type, list}){
      debug('addActor ', type, list)
  
      if(!await this.isOwner()){ throw new Error('must be owner to manipulate bucket metadata') }
      
      if(['meta', 'readers', 'writers'].indexOf(type) < 0){
        throw new Error('addActor type invalid ['+type+']')
      }
  
      const emailFprs = await this.root.keychain.resolveEmails(list)
  
      const fullList = [].concat( list, emailFprs )
  
      if(!this.metadata[type]){  this.metadata[type] = [] }
  
      this.metadata[type] = Utils.uniqueArray( this.metadata[type].concat( fullList ) )
  
      await this.setMetadata()
    }
  }
  
  module.exports = Bucket
  