import {ZipArchive} from 'archiver';
import {Readable} from 'node:stream';
import {finished} from 'node:stream/promises';
import {readdir,lstat,realpath} from 'node:fs/promises';
import {fileURLToPath} from 'node:url';
import {resolve,sep} from 'node:path';
import {createHash} from 'node:crypto';
import {safeEntryName} from './validation.mjs';
import {getScenario,FIXED_DATE,TEXT,CONTACTS} from './scenarios.mjs';
const FIXTURES=fileURLToPath(new URL('../fixtures/',import.meta.url));
export async function checkedFixtureDirectory(relative){
 if(!['delivery','exports'].includes(relative))throw new Error('Каталог отсутствует в разрешённом списке.');
 const dir=resolve(FIXTURES,relative),base=await realpath(FIXTURES),actual=await realpath(dir);if(!actual.startsWith(base+sep))throw new Error('Каталог вышел за пределы fixtures.');
 let count=0,total=0;async function walk(path){for(const item of await readdir(path,{withFileTypes:true})){const p=resolve(path,item.name),st=await lstat(p);if(st.isSymbolicLink())throw new Error('Символические ссылки в учебных fixtures запрещены.');if(st.isDirectory())await walk(p);else if(st.isFile()){count++;total+=st.size;if(count>30||total>1048576)throw new Error('Fixtures превышают учебные ограничения.');}else throw new Error('Допускаются только обычные файлы и каталоги.');}}await walk(dir);return dir;
}
async function orderedDirectory(dir){
 const entries=[];async function visit(path,relative=''){const children=await readdir(path,{withFileTypes:true});children.sort((a,b)=>a.name<b.name?-1:a.name>b.name?1:0);for(const child of children){const absolute=resolve(path,child.name),name=relative+child.name,stats=await lstat(absolute);if(stats.isSymbolicLink())throw new Error('Символическая ссылка в каталоге запрещена.');safeEntryName(name);if(!stats.isFile()&&!stats.isDirectory())throw new Error('Недопустимый тип файла.');entries.push({absolute,name,stats});if(stats.isDirectory())await visit(absolute,name+'/');}}await visit(dir);return entries;
}
export async function writeArchive(output,{scene,variant},{signal,onProgress,onSourceChunk}={}){
 const completion=finished(output,{cleanup:true});completion.catch(()=>{});let v,directory,ordered;
 try{v=getScenario(scene,variant).variant;if(v.error)safeEntryName(v.error);if(signal?.aborted)throw signal.reason||new Error('Операция отменена.');directory=scene==='directory'?await checkedFixtureDirectory('delivery'):scene==='glob'?await checkedFixtureDirectory('exports'):null;ordered=scene==='directory'?await orderedDirectory(directory):null;if(signal?.aborted)throw signal.reason||new Error('Операция отменена.');}catch(error){output.destroy(error);await completion.catch(()=>{});throw error;}
 const archive=new ZipArchive({zlib:{level:v.level??6},store:!!v.store,statConcurrency:1});const owned=new Set(),events=[];let failed=false;
 const fail=error=>{if(failed)return;failed=true;for(const source of owned)source.destroy(error);archive.abort();archive.destroy(error);output.destroy(error);};
 archive.on('warning',fail);archive.on('error',fail);output.on('error',fail);
 const abort=()=>fail(signal.reason instanceof Error?signal.reason:new Error('Операция отменена.'));signal?.addEventListener('abort',abort,{once:true});
 archive.on('entry',entry=>events.push({name:entry.name}));archive.on('progress',p=>onProgress?.({entries:p.entries.processed,total:p.entries.total,bytes:archive.pointer()}));
 archive.pipe(output);
 const append=(content,name)=>archive.append(content,{name:safeEntryName(name),date:FIXED_DATE,mode:0o644});
 try{
  if(scene==='memory'||scene==='http'){
   append(TEXT,'README.txt');append(Buffer.from(JSON.stringify(CONTACTS,null,2)+'\n'),'report.json');if(variant==='csv')append('id,name,amount\nA-101,Анна,18000\nA-102,Борис,24000\n','report.csv');
  }else if(scene==='directory'){const prefix=variant==='root'?'':'client-pack/';for(const entry of ordered){const name=prefix+entry.name;if(entry.stats.isDirectory())archive.append('',{name:name+'/',type:'directory',date:FIXED_DATE,mode:0o755});else archive.file(entry.absolute,{name,date:FIXED_DATE,mode:0o644,stats:entry.stats});}}
  else if(scene==='glob')archive.glob(variant==='monthly'?'monthly/*.csv':'**/*.csv',{cwd:directory,dot:false,ignore:['private/**']},{prefix:'reports',date:FIXED_DATE,mode:0o644});
  else if(scene==='compression')append(TEXT.repeat(800),'report.txt');
  else if(scene==='manifest'){
   const files=[{name:variant==='flat'?'invoice-101.txt':'invoices/invoice-101.txt',text:'Учебный счёт A-101: 18000 RUB\n'},{name:variant==='flat'?'contacts.json':'data/contacts.json',text:JSON.stringify(CONTACTS,null,2)+'\n'}];
   for(const f of files)append(f.text,f.name);append(JSON.stringify({schema:1,files:files.map(f=>({name:f.name,bytes:Buffer.byteLength(f.text),sha256:createHash('sha256').update(f.text).digest('hex')}))},null,2)+'\n','manifest.json');
  }else if(scene==='names')append('Корректное относительное имя.\n','reports/отчёт.txt');
  else if(scene==='abort'){
   const source=Readable.from((async function*(){for(let i=0;i<10;i++){if(signal?.aborted)break;yield Buffer.from(TEXT.repeat(20));onSourceChunk?.(i+1);await new Promise(r=>setTimeout(r,5));}})());owned.add(source);source.on('error',fail);append(source,'stream.txt');
  }
  const finalization=archive.finalize();await Promise.all([completion,finalization]);return{bytes:archive.pointer(),entries:events,cancelled:false};
 }catch(error){fail(error);throw error;}finally{signal?.removeEventListener('abort',abort);for(const source of owned)if(!source.destroyed)source.destroy();}
}
