import { Hono } from 'hono';
import { cors } from 'hono/cors';
import { bodyLimit } from 'hono/body-limit';
import { streamSSE } from 'hono/streaming';

export const MAX_BODY = 1024;
export const DEMO_TOKEN = 'demo-anna'; // Public teaching token, NEVER a production secret.
const services = ['Настройка сайта','Аудит интерфейса','Поддержка API'];
export function initialData() {
  return Array.from({length:18},(_,i)=>({id:i+1,title:`${services[i%3]} №${i+1}`,owner:i%2?'boris':'anna',status:i%3?'open':'done',budget:5000+i*750}));
}
export function createApi({onEvent=()=>{}, onInternalError=()=>{}, streamDelay=280}={}) {
  const app = new Hono();
  let records=initialData();
  const fail=(c,status,code,message,fields)=>c.json({ok:false,error:{code,message,...(fields?{fields}:{})},requestId:c.get('requestId')},status);
  const note=(c,stage)=>onEvent({requestId:c.get('requestId'),stage});
  app.use('*',async(c,next)=>{
    const started=performance.now();c.set('requestId',crypto.randomUUID());note(c,'1 · requestId: до');
    await next();
    c.header('X-Request-Id',c.get('requestId'));c.header('X-Content-Type-Options','nosniff');c.header('Cache-Control','no-store');
    c.header('Server-Timing',`app;dur=${(performance.now()-started).toFixed(2)}`);note(c,'5 · requestId: после');
  });
  app.use('/api/*',cors({origin:'https://demo.example',allowMethods:['GET','POST','OPTIONS'],allowHeaders:['Content-Type','Authorization'],exposeHeaders:['X-Request-Id'],maxAge:600}));
  app.use('/api/*',async(c,next)=>{note(c,'2 · журнал: до');await next();note(c,'4 · журнал: после');});
  app.use('/api/*',bodyLimit({maxSize:MAX_BODY,onError:c=>fail(c,413,'BODY_TOO_LARGE','Тело запроса превышает 1024 байта.')}));
  app.get('/api/health',c=>{note(c,'3 · обработчик health');return c.json({ok:true,framework:'Hono',mode:'shared-router',records:records.length});});
  app.get('/api/jobs',c=>{
    note(c,'3 · обработчик списка');
    const {page='1',limit='5',q='',status='all'}=c.req.query();
    if(!/^[1-9]\d{0,3}$/.test(page)||!/^(?:[1-9]|10)$/.test(limit)||q.length>80||!['all','open','done'].includes(status))return fail(c,422,'QUERY_INVALID','page: 1–9999, limit: 1–10, q: до 80 символов, status: all/open/done.');
    const rows=records.filter(x=>(status==='all'||x.status===status)&&x.title.toLowerCase().includes(q.toLowerCase()));
    const p=Number(page),l=Number(limit),start=(p-1)*l;
    return c.json({ok:true,data:rows.slice(start,start+l),meta:{page:p,limit:l,total:rows.length,pages:Math.ceil(rows.length/l),next:start+l<rows.length?p+1:null}});
  });
  app.get('/api/jobs/:id',c=>{
    note(c,'3 · обработчик карточки');const id=c.req.param('id');
    if(!/^[1-9]\d{0,5}$/.test(id))return fail(c,422,'ID_INVALID','Идентификатор должен быть положительным целым числом.');
    const row=records.find(x=>x.id===Number(id));return row?c.json({ok:true,data:row}):fail(c,404,'JOB_NOT_FOUND','Такой учебной заявки нет.');
  });
  app.post('/api/jobs',async c=>{
    note(c,'3 · обработчик JSON');
    if((c.req.header('Content-Type')||'').split(';')[0].trim().toLowerCase()!=='application/json')return fail(c,415,'CONTENT_TYPE','Используйте Content-Type: application/json.');
    let data;try{data=await c.req.json();}catch{return fail(c,400,'JSON_INVALID','JSON содержит синтаксическую ошибку.');}
    const fields={};
    if(!data||typeof data!=='object'||Array.isArray(data))return fail(c,422,'VALIDATION','Ожидается объект заявки.');
    if(typeof data.title!=='string'||data.title.trim().length<3||data.title.trim().length>80)fields.title='Название: от 3 до 80 символов.';
    if(!Number.isInteger(data.budget)||data.budget<0||data.budget>1000000)fields.budget='Бюджет: целое число от 0 до 1 000 000.';
    if(Object.keys(data).some(k=>!['title','budget'].includes(k)))fields.extra='Допустимы только title и budget; owner назначает сервер.';
    if(Object.keys(fields).length)return fail(c,422,'VALIDATION','Исправьте поля заявки.',fields);
    if(records.length>=50)return fail(c,409,'DEMO_FULL','Достигнут лимит демо: 50 записей. Сбросьте мастерскую или перезапустите сервер.');
    const row={id:Math.max(...records.map(x=>x.id))+1,title:data.title.trim(),budget:data.budget,owner:'anna',status:'open'};records.push(row);
    c.header('Location',`/api/jobs/${row.id}`);return c.json({ok:true,data:row,storage:'process-memory'},201);
  });
  app.use('/api/private/*',async(c,next)=>{
    note(c,'3 · учебная аутентификация');
    if(c.req.header('Authorization')!==`Bearer ${DEMO_TOKEN}`){c.header('WWW-Authenticate','Bearer realm="Hono demo"');return fail(c,401,'AUTH_REQUIRED','Нужен открытый учебный токен demo-anna.');}
    c.set('user',{id:'anna'});await next();
  });
  app.get('/api/private/jobs/:id',c=>{
    const row=records.find(x=>String(x.id)===c.req.param('id'));
    if(!row)return fail(c,404,'JOB_NOT_FOUND','Такой учебной заявки нет.');
    if(row.owner!==c.get('user').id)return fail(c,403,'FORBIDDEN','Заявка принадлежит другому пользователю.');
    return c.json({ok:true,data:row,user:c.get('user').id});
  });
  app.get('/api/failure',()=>{throw new Error('Synthetic internal failure: never returned to client');});
  app.get('/api/progress',c=>{
    const raw=c.req.query('steps')||'5';if(!/^[3-8]$/.test(raw))return fail(c,422,'STEPS_INVALID','steps: целое число от 3 до 8.');
    c.header('X-Accel-Buffering','no');const steps=Number(raw);
    return streamSSE(c,async stream=>{
      stream.onAbort(()=>note(c,'Поток отменён читателем'));
      try {
        for(let i=1;i<=steps&&!stream.aborted;i++){
          await stream.sleep(streamDelay);if(stream.aborted)break;
          await stream.writeSSE({event:'progress',id:String(i),data:JSON.stringify({step:i,total:steps,percent:Math.round(i/steps*100),message:`Проверена порция ${i} из ${steps}`})});
        }
        if(!stream.aborted)await stream.writeSSE({event:'done',data:JSON.stringify({ok:true,message:'Учебный поток завершён. Это не фоновая очередь.'})});
      } catch(err) {
        onInternalError({requestId:c.get('requestId'),error:String(err?.message||err)});
        // Catch inside the callback: v4 helper's third-argument error handler
        // also writes the original exception message after calling that handler.
        if(!stream.aborted)try{await stream.writeSSE({event:'error',data:JSON.stringify({message:'Ошибка потока'})});}catch{/* Reader already disconnected. */}
      }
    });
  });
  app.notFound(c=>fail(c,404,'ROUTE_NOT_FOUND','Маршрут не найден.'));
  app.onError((err,c)=>{onInternalError({requestId:c.get('requestId'),error:err.message});return fail(c,500,'INTERNAL','Не удалось выполнить операцию. Сохраните requestId.');});
  return app;
}
