const e=require("path"),t=require("fs"),a=require("hypercore-blob-server"),i=require("hyperblobs"),r=require("ready-resource"),n=require("safety-catch"),o=require("hypercore-id-encoding"),s=require("speedometer"),{pipelinePromise:l,Readable:c,Transform:d,pipeline:y}=require("streamx"),h=require("b4a"),{KeetErrorV2:p}=require("../../errors");function u(e){return"string"==typeof e&&!e.includes("\0")&&!e.split(/[/\\]/).includes("..")}module.exports=class extends r{constructor(e,t={}){super(),this.keet=e,this.local=e.local,this.store=e.store,this.server=new a(this.store.session(),{resolve:this._resolveEncryptionKey.bind(this)}),this._activeDownloads=new Map,this._onerror=t.onerror||n,this.ready().catch(this._onerror)}_open(){return this.server.listen()}_close(){return this.server.close()}async _resolveEncryptionKey(e,{blob:t,filename:a,drive:i,version:r}){const n=i?"@local/drive-files":"@local/blob-files",o={pointer:{externalDrive:{key:i,path:a&&a.startsWith("/")?a.slice(1):a,version:r},externalBlob:{key:e,blob:t}}},s=await this.local.get(n,{file:o});return s&&await this.keet.internalBlobsActiveInRoom(s.roomKey),s?{key:e,encryptionKey:s.encryptionKey}:null}_getLinkOptions(e){if(!e.pointer?.externalBlob&&!e.pointer?.externalDrive)throw new Error("Invalid file pointer");return e.pointer.externalBlob?{collection:"@local/blob-files",key:e.pointer.externalBlob.key,opts:{blob:e.pointer.externalBlob.blob,type:e.metadata?.mimetype}}:{collection:"@local/drive-files",key:e.pointer.externalDrive.key,opts:{filename:e.pointer.externalDrive.path,version:e.pointer.externalDrive.version}}}async _clearAllFiles(e,t,a,{leaving:i=!1}={}){const r=t?"@local/blob-files":"@local/drive-files",o=t?"@local/blob-files-by-room":"@local/drive-files-by-room",s=[];for await(const t of e.find(o,{gte:{roomKey:a},lte:{roomKey:a}})){if(t.cleared)continue;const{key:a,opts:o}=this._getLinkOptions(t.file),l=this.server.clear(a,o);l.catch(n),s.push(l),i?await e.delete(r,t):await e.insert(r,{...t,cleared:!0})}return(await Promise.allSettled(s)).filter(e=>"fulfilled"===e.status).reduce((e,t)=>e+t.value,0)}async _findAndClearOldFiles(e,t){const a=[];for await(const i of this.local.find(e,{lte:{lastAccessed:t}}))i.cleared||a.push(i);const i=[];for(const e of a){const{key:t,opts:a}=this._getLinkOptions(e.file),r=this.server.clear(t,a);r.catch(n),i.push(r)}return{recordsToClear:a,bytesCleared:(await Promise.allSettled(i)).filter(e=>"fulfilled"===e.status).reduce((e,t)=>e+t.value,0)}}async _createFile(e,t,a){const i={metadata:a,pointer:{externalBlob:{key:e.externalBlobs.key,blob:t}}};i.metadata?.userdata&&(i.metadata.userdata=h.from(i.metadata.userdata,"base64"));const r=await this.keet.internalEnterLocalTransaction();try{const t={file:i,roomKey:e.key,encryptionKey:e.encryptionKey,cleared:!1,own:!0,lastAccessed:Date.now()};await r.insert("@local/blob-files",t),await this._updateFileStats(r,t),await r.flush()}finally{await this.keet.internalExitLocalTransaction()}return i}async saveFile(a,i,r){if(this.opened||await this.ready(),!u(i))throw p.INVALID_FILE_PATH();const n=e.basename(i),o=this.keet.internalOpenExistingRoom(a);try{await this.keet.internalBlobsActiveInRoom(a);const e=t.createReadStream(i),s=o.externalBlobs.createWriteStream(r);return await l(e,s),this._createFile(o,s.id,{name:n,...r})}finally{this.keet.internalReleaseRoom(o)}}subscribeAddFile(a,i,r={}){if(!u(i))throw p.INVALID_FILE_PATH();const o=e.basename(i),s=this,l=new c;return t.stat(i,(e,c)=>{if(l.destroyed)return;if(e)return l.destroy(e);let d=0;const y=c.size,h=t.createReadStream(i),p=s.keet.internalOpenExistingRoom(a),u=p.externalBlobs.createWriteStream(r);this.keet.internalBlobsActiveInRoom(a).catch(n);let w=setInterval(()=>{l.destroyed||d<y&&l.push({bytesAdded:d,byteLength:y,result:null})},r?.debounceTime??500);h.pipe(u),h.on("data",e=>{d+=e.byteLength}),h.on("error",e=>l.destroy(e)),u.on("close",async()=>{if(w&&clearInterval(w),w=null,!l.destroyed)try{const e=await s._createFile(p,u.id,{name:o,...r});l.push({bytesAdded:d,byteLength:y,result:e}),l.push(null)}catch(e){l.destroy(e)}}),l.on("error",e=>{h.destroy(e)}),l.on("close",()=>{w&&clearInterval(w),w=null,h.destroy(),u.destroy(),s.keet.internalReleaseRoom(p)}),r.onFileStat&&r.onFileStat()}),l}async saveFileBlob(e,t,a){this.opened||await this.ready();const i=this.keet.internalOpenExistingRoom(e);try{await this.keet.internalBlobsActiveInRoom(e);const r=await i.externalBlobs.put(t);return this._createFile(i,r,a)}finally{this.keet.internalReleaseRoom(i)}}async clearAllFiles(e,t){let a=0,i=0;const r=await this.keet.internalEnterLocalTransaction();try{a=await this._clearAllFiles(r,!0,e,t),i=await this._clearAllFiles(r,!1,e,t),await r.insert("@local/file-stats",{roomKey:e,total:0}),await r.flush()}finally{await this.keet.internalExitLocalTransaction()}return{bytesCleared:a+i}}async clearOldFiles(){const e=Date.now()-6048e5,t=new Set,a=await this._findAndClearOldFiles("@local/blob-files-by-last-accessed",e),i=await this._findAndClearOldFiles("@local/drive-files-by-last-accessed",e),r=await this.keet.internalEnterLocalTransaction();try{for(const e of a.recordsToClear)await r.insert("@local/blob-files",{...e,cleared:!0}),await this._updateFileStats(r,{...e,cleared:!0}),t.add(o.encode(e.roomKey));for(const e of i.recordsToClear)await r.insert("@local/drive-files",{...e,cleared:!0}),await this._updateFileStats(r,{...e,cleared:!0}),t.add(o.encode(e.roomKey));await r.insert("@local/file-gc-info",{lastCleared:Date.now()}),await r.flush()}finally{await this.keet.internalExitLocalTransaction()}for(const e of t)this.keet.emit("local-files-changed",e);return{bytesCleared:a.bytesCleared+i.bytesCleared}}async _ensureLocalFile(e,t,a){await this.keet.internalBlobsActiveInRoom(t);const i=await this.keet.internalEnterLocalTransaction();try{const r=await i.get(e,{roomKey:t,file:a});let n=r?.encryptionKey;if(!n){const e=await i.get("@local/rooms",{roomKey:t});n=e?.encryptionKey}const s={...r,cleared:r?.cleared??!1,roomKey:t,encryptionKey:n,file:a,lastAccessed:Date.now()};return await i.insert(e,s),r||await this._updateFileStats(i,s),await i.flush(),r||this.keet.emit("local-files-changed",o.encode(t)),s}finally{await this.keet.internalExitLocalTransaction()}}async _loadBlob(e,t){const a=this.keet.internalOpenExistingRoom(e);try{return await a.ready(),await a.engine.getBlob(t)}finally{this.keet.internalReleaseRoom(a)}}async getFileLink(e,t){if(this.opened||await this.ready(),t.pointer.blob){const a=await this._loadBlob(e,t.pointer);return this.server.getBlobLink(a,{type:t.metadata?.mimetype})}const{collection:a,key:i,opts:r}=this._getLinkOptions(t);return await this._ensureLocalFile(a,e,t),this.server.getLink(i,r)}async getFileEntry(e,t,a={}){if(!t)return null;this.opened||await this.ready();const i={roomKey:e,encryptionKey:null,cleared:!1,httpLink:null};if(!(t.pointer.externalBlob||t.pointer.externalDrive||t.pointer.blob||t.pointer.bee))return i;if(t.pointer.blob||t.pointer.bee){const r=a.loadBlob?await a.loadBlob(t.pointer):await this._loadBlob(e,t.pointer);return i.httpLink=this.server.getBlobLink(r,{type:t.metadata?.mimetype}),i}const{collection:r,key:n,opts:o}=this._getLinkOptions(t);return{...await this._ensureLocalFile(r,e,t),httpLink:this.server.getLink(n,o)}}async getFileEntryWithPreview(e,t,a={}){this.opened||await this.ready();const i=await this.getFileEntry(e,t,a);if(!t.preview)return i;const[r,n,o]=await Promise.all([this.getFileEntry(e,t.preview.small,a),this.getFileEntry(e,t.preview.medium,a),this.getFileEntry(e,t.preview.large,a)]);return i.preview={small:r,medium:n,large:o},i}async getFileCleared(e,t){const{collection:a}=this._getLinkOptions(t),i=await this.local.get(a,{file:t});return!!i&&i.cleared}async getLocalFiles(e){return(await Promise.all([this.local.find("@local/blob-files-by-room",{gte:{roomKey:e},lte:{roomKey:e}}).toArray(),this.local.find("@local/drive-files-by-room",{gte:{roomKey:e},lte:{roomKey:e}}).toArray()])).flat()}async clearFile(e,t){if(!t?.pointer?.externalBlob&&!t?.pointer?.externalDrive)return;const{collection:a,key:i,opts:r}=this._getLinkOptions(t),n=await this.keet.internalEnterLocalTransaction();try{const o=await n.get(a,{roomKey:e,file:t});if(!o||o.cleared)return;await this.server.clear(i,r);const s={roomKey:e,file:t,lastAccessed:o.lastAccessed??Date.now(),cleared:!0};await n.insert(a,s),await this._updateFileStats(n,s),await n.flush()}finally{await this.keet.internalExitLocalTransaction()}}async unclearFile(e,t){const{collection:a}=this._getLinkOptions(t),i=await this.keet.internalEnterLocalTransaction();try{const r=await i.get(a,{roomKey:e,file:t});if(!r||!r.cleared)return;const n={roomKey:e,file:t,lastAccessed:Date.now(),cleared:!1};await i.insert(a,n),await this._updateFileStats(i,n),await i.flush()}finally{await this.keet.internalExitLocalTransaction()}}async startDownload(e,t){this.opened||await this.ready();const{key:a,opts:i}=this._getLinkOptions(t),r=this.server.getLink(a,{...i,url:!1}),n=this.server.download(a,i);return this._activeDownloads.set(r,n),n.done().then(()=>{this._activeDownloads.get(r)===n&&this._activeDownloads.delete(r)}),r}async cancelDownload(e,t){this.opened||await this.ready();const a=this._activeDownloads.get(t);t&&a&&await a.close().finally(()=>{this._activeDownloads.get(t)===a&&this._activeDownloads.delete(t)})}readFileStream(e,t,{includeProgress:a=!1}={}){if(!t.pointer.externalBlob)throw new Error("Only external blob files support streaming");const i=this,r=new c({open(n){i._openReadStream(e,t,r,a).then(n,n)}});return r}async _openReadStream(e,t,a,r){this.opened||await this.ready();const o=t.pointer.externalBlob.blob,l=t.pointer.externalBlob.key,{collection:c}=this._getLinkOptions(t),d=await this._ensureLocalFile(c,e,t),y=r?s():null,h=this.store.get({key:l,encryptionKey:d.encryptionKey,encrypted:!0,active:!1});await h.ready();const p=new i(h).createReadStream(o);let u=0;p.on("data",e=>{if(!r)return void a.push(e);u+=e.length;const t=y(e.length);a.push({data:e,speed:t,progress:{bytesRead:u,byteLength:o.byteLength,percentage:Math.round(u/o.byteLength*100)}})}),p.on("end",()=>{a.push(null)}),p.on("error",e=>a.destroy(e)),a.once("close",()=>{p.destroy(),h.close().catch(n)})}subscribeSaveToDisk(e,a,i,{progressInterval:r=50}={}){if(this.opened||this.ready(),!u(i))throw p.INVALID_FILE_PATH();const n=this.readFileStream(e,a,{includeProgress:!0}),o=t.createWriteStream(i),s=new c;s.once("close",()=>o.destroy()),s.once("error",e=>o.destroy(e));let l=0;return y(n,new d({transform(e,t){if(s.destroyed)return t(null);const{data:a,...i}=e,n=Date.now();i.done=i.progress.bytesRead===i.progress.byteLength,(i.done||n-l>r)&&(l=n,s.push(i)),this.push(a),t(null)}}),o,e=>{s.destroyed||(s.push(null),e&&s.destroy(e))}),s}async saveToDisk(e,a,i){if(this.opened||await this.ready(),!u(i))throw p.INVALID_FILE_PATH();const r=this.readFileStream(e,a),n=t.createWriteStream(i);await l(r,n)}async refreshLinks(e){this.opened||await this.ready();const t=[];for(const a of e)t.push(this.server.refreshLink(a));return t}async monitor(e,t){this.opened||await this.ready();const{key:a,opts:i}=this._getLinkOptions(t);return this.server.monitor(a,i)}async _updateFileStats(e,{roomKey:t,file:a,cleared:i}){const r=a.pointer.externalBlob?.blob.byteLength;if(!r)return;let n=await e.get("@local/file-stats",{roomKey:t});n||(n={roomKey:t,total:0});const o=i?-r:r;n.total=Math.max(n.total+o,0),await e.insert("@local/file-stats",n)}};