import { request, Pool, MockAgent, Agent, Dispatcher1Wrapper } from 'undici';
import { channel } from 'node:diagnostics_channel';
import { mkdtemp, readdir, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { startUpstream, FILE_SHA } from './upstream.mjs';
import { getWithRetry, download, safeLog } from './client.mjs';

export const definitions = [
  {id:'get',title:'GET: fetch и request',use:'Каталог услуг для админки',goal:'Сравните ответ 200 с 404: HTTP-ошибка не равна сетевой.',variants:[['request','request · 200'],['fetch','fetch · 200'],['missing','request · 404']],takeaway:'Проверяйте HTTP-статус и отдельно обрабатывайте сетевые ошибки. Тело читается один раз.'},
  {id:'post',title:'POST без двойного заказа',use:'Оплата или заявка после повторного клика',goal:'Повторите ключ с тем же и с другим содержимым.',variants:[['same','Тот же ключ и сумма'],['conflict','Тот же ключ, другая сумма']],takeaway:'Idempotency-Key работает только по контракту сервера. При смене тела нужен новый ключ.'},
  {id:'pool',title:'Пул соединений',use:'Пакет из шести обращений к одному API',goal:'Сравните одно и три соединения по реальным событиям сервера.',variants:[['one','1 соединение'],['three','3 соединения']],takeaway:'Pool ограничивает число соединений к одному origin. Очередь приложения тоже должна быть ограничена.'},
  {id:'timeouts',title:'Таймауты и отмена',use:'Пользователь ушёл со страницы; API перестал отвечать',goal:'Отделите ожидание заголовков от паузы в теле и ручной отмены.',variants:[['headers','Заголовки опоздали'],['body','Тело остановилось'],['cancel','Отмена пользователем']],takeaway:'headersTimeout и bodyTimeout контролируют разные фазы. Общий дедлайн задаётся AbortSignal.'},
  {id:'stream',title:'Файл потоком',use:'Выгрузка отчёта без хранения всего файла в RAM',goal:'Проверьте успешный файл, ограничение размера и очистку после отмены.',variants:[['full','Файл 256 КиБ'],['limit','Лимит 32 КиБ'],['cancel','Прервать загрузку']],takeaway:'pipeline передаёт backpressure, лимит считается по фактическим байтам, .part удаляется при неудаче.'},
  {id:'body',title:'Пустой и неверный JSON',use:'Интеграция с API с неполным контрактом',goal:'Посмотрите, почему 204 нельзя автоматически разбирать как JSON.',variants:[['invalid','Невалидный JSON'],['empty','204 без тела'],['consumed','Повторное чтение тела']],takeaway:'HTTP-успех не гарантирует корректный JSON. Определяйте формат и политику пустого ответа явно.'},
  {id:'retry',title:'Повторы GET',use:'Временная перегрузка внешнего сервиса',goal:'Наблюдайте историю попыток и исчерпание бюджета.',variants:[['503','503 → 503 → 200'],['429','429 → 429 → 200'],['exhaust','Только две попытки']],takeaway:'Повторы ограничены методом, числом попыток и общим дедлайном. Retry-After нельзя игнорировать.'},
  {id:'mock',title:'MockAgent',use:'Тест интеграции без внешнего API',goal:'Разрешите один контракт и попробуйте незаданный маршрут.',variants:[['ok','Ожидаемый ответ'],['blocked','Неожиданный маршрут']],takeaway:'disableNetConnect запрещает случайный выход в сеть. Передайте dispatcher именно тестируемому клиенту.'},
  {id:'diagnostics',title:'Наблюдение за запросом',use:'Поиск задержки между вашими сервисами',goal:'Сопоставьте технические события с безопасной записью журнала.',variants:[['ok','Успех 200'],['missing','Ответ 404']],takeaway:'Логируйте разрешённые поля, а не весь запрос. Секреты заголовков и параметры URL не нужны в журнале.'}
];
export async function runScenario(id, variant) {
  const def = definitions.find(x => x.id === id);
  if (!def?.variants.some(x => x[0] === variant)) throw new Error('Unknown scenario');
  const started = performance.now(), events = [];
  const mark = (label, data = {}) => events.push({ ms: Math.round((performance.now() - started) * 10) / 10, label, ...data });
  const fixture = await startUpstream(); const agent = new Agent({ connections: 2 });
  let result;
  try {
    mark('Начало сценария'); const url = path => fixture.origin + path;
    if (id === 'get') {
      if (variant === 'fetch') {
        const res = await fetch(url('/catalog'), { dispatcher: new Dispatcher1Wrapper(agent), signal: AbortSignal.timeout(2000) });
        result = { api: 'global fetch', status: res.status, ok: res.ok, contentType: res.headers.get('content-type'), data: await res.json(), builtInUndici: process.versions.undici };
      } else {
        const res = await request(url(variant === 'missing' ? '/missing' : '/catalog'), { dispatcher: agent, signal: AbortSignal.timeout(2000) });
        result = { api: 'undici.request', status: res.statusCode, ok: res.statusCode >= 200 && res.statusCode < 300, contentType: res.headers['content-type'], data: await res.body.json() };
      }
      mark('Тело прочитано', { status: result.status });
    }
    if (id === 'post') {
      const responses = [];
      for (const amount of [2500, variant === 'same' ? 2500 : 4900]) {
        const res = await request(url('/orders'), { dispatcher: agent, method:'POST', headers:{'content-type':'application/json','idempotency-key':'demo-order-42'}, body:JSON.stringify({amount}), signal:AbortSignal.timeout(2000) });
        const response = { status:res.statusCode, replay:res.headers['x-idempotent-replay'] === 'true', data:await res.body.json() }; responses.push(response); mark('Ответ на POST', {status:response.status});
      }
      result={responses,ordersCreated:fixture.state.orders.size};
    }
    if (id === 'pool') {
      const connections=variant==='one'?1:3, pool=new Pool(fixture.origin,{connections,pipelining:1});
      const base=performance.now();
      try {
        const responses=await Promise.all(Array.from({length:6},(_,i)=>pool.request({path:`/work?id=${i+1}`,method:'GET',signal:AbortSignal.timeout(4000)}).then(async res=>({status:res.statusCode,data:await res.body.json()}))));
        result={connections,requests:responses.length,maxActiveOnServer:fixture.state.maxActive,elapsedMs:Math.round(performance.now()-base),responses};
        for(const e of fixture.state.events)events.push({ms:Math.round((e.at-started)*10)/10,label:`Запрос ${e.id}: ${e.phase==='start'?'начало':'конец'}`,active:e.active});
      } finally {await pool.close();}
      result.poolClosed=pool.closed;
    }
    if (id === 'timeouts') {
      const controller=new AbortController(); let timer;
      if(variant==='cancel')timer=setTimeout(()=>controller.abort(),45);
      try {
        const res=await request(url(variant==='body'?'/slow-body':'/slow-headers'),{dispatcher:agent,headersTimeout:variant==='headers'?50:1000,bodyTimeout:variant==='body'?50:1000,signal:variant==='cancel'?controller.signal:AbortSignal.timeout(2000)});
        await res.body.text(); result={unexpectedSuccess:true};
      } catch(error){result={errorName:error.name,errorCode:error.code,phase:variant};mark('Операция остановлена',{errorCode:error.code});} finally{clearTimeout(timer);}
    }
    if(id==='stream') {
      const directory=await mkdtemp(join(tmpdir(),'aen-undici-')); const destination=join(directory,'report.bin'); const controller=new AbortController();
      try {
        result=await download(url('/file'),destination,{dispatcher:agent,maxBytes:variant==='limit'?32768:1048576,signal:controller.signal,onProgress:({bytes})=>{mark('Получена порция',{bytes});if(variant==='cancel'&&bytes>=32768)controller.abort();}});
        result.expectedSha256=FILE_SHA;result.hashMatches=result.sha256===FILE_SHA;
      } catch(error){result={complete:false,errorCode:error.code,errorName:error.name};}
      result.filesAfter=await readdir(directory);result.partialRemoved=!result.filesAfter.some(x=>x.endsWith('.part'));await rm(directory,{recursive:true,force:true});
    }
    if(id==='body') {
      const path=variant==='invalid'?'/invalid-json':variant==='empty'?'/empty':'/catalog';
      const res=await request(url(path),{dispatcher:agent,signal:AbortSignal.timeout(2000)});
      result={status:res.statusCode};
      if(variant==='empty'){result.data=null;result.policy='204 is intentionally treated as no representation';await res.body.dump();}
      else{try{result.first=await res.body.json();if(variant==='consumed')await res.body.text();}catch(error){result.errorName=error.name;result.errorCode=error.code??null;result.message=error.message;}}
    }
    if(id==='retry')result=await getWithRetry(url(`/retry?kind=${variant==='429'?'429':'503'}`),{dispatcher:agent,maxAttempts:variant==='exhaust'?2:3,onAttempt:attempt=>mark('Попытка GET',attempt)});
    if(id==='mock') {
      const mock=new MockAgent();mock.disableNetConnect();
      mock.get('https://catalog.test').intercept({path:'/catalog',method:'GET'}).reply(200,{items:[{id:42,name:'Мок'}]});
      try {
        const res=await request(`https://catalog.test/${variant==='ok'?'catalog':'unexpected'}`,{dispatcher:mock});result={status:res.statusCode,data:await res.body.json()};mock.assertNoPendingInterceptors();result.pending=0;
      }catch(error){result={errorName:error.name,errorCode:error.code,pending:mock.pendingInterceptors().length,realNetworkDisabled:true};}finally{await mock.close();}
    }
    if(id==='diagnostics') {
      const subscriptions=[];const path=variant==='missing'?'/missing':'/catalog';
      for(const name of ['undici:request:create','undici:request:headers','undici:request:trailers']){
        const c=channel(name);const fn=({request:req,response})=>{if(String(req.origin)===fixture.origin)mark(name,{status:response?.statusCode??null});};c.subscribe(fn);subscriptions.push([c,fn]);
      }
      try{
        const res=await request(url(path+'?private=not-for-logs'),{dispatcher:agent,headers:{authorization:'Bearer demo-never-log-this','x-request-id':'demo-42'},signal:AbortSignal.timeout(2000)});await res.body.dump();
        result={log:safeLog({id:'demo-42',path:path+'?private=not-for-logs',status:res.statusCode,ms:performance.now()-started}),fields:'allowlist only: requestId, route, status, durationMs, errorCode'};
      }finally{for(const[c,fn]of subscriptions)c.unsubscribe(fn);}
    }
    mark('Сценарий завершён');
    return {id,variant,result,events:events.sort((a,b)=>a.ms-b.ms),elapsedMs:Math.round(performance.now()-started),mechanism:'recorded Node.js execution'};
  } finally {await agent.close();await fixture.close();}
}
