// tools/logViewer.js (Pino-compatible JSONL + timearea + filters)
const fs = require('fs');
const path = require('path');
const os = require('os');
const readline = require('readline');

function resolveLatestLogFile(dir = path.resolve(process.cwd(), 'logs')) {
  if (!fs.existsSync(dir)) throw new Error(`Logs directory not found: ${dir}`);
  const files = fs.readdirSync(dir)
    .map(f => ({ file: f, stat: fs.statSync(path.join(dir, f)) }))
    .filter(f => f.stat.isFile())
    .sort((a, b) => b.stat.mtimeMs - a.stat.mtimeMs);
  if (!files.length) throw new Error(`No log files in ${dir}`);
  return path.join(dir, files[0].file);
}

let LOG_FILE = process.env.LOG_FILE
  ? path.resolve(process.cwd(), process.env.LOG_FILE)
  : resolveLatestLogFile();

const MAX_SAFE_READ_MB = 50;
const DEFAULT_LAST_LINES = 200;

function setLogFile(filePath) { LOG_FILE = path.resolve(process.cwd(), filePath); }

function MB(n){return n*1024*1024;}
function toInt(v,d){const n=parseInt(v,10);return Number.isFinite(n)?n:d;}

const LEVEL_MAP_NUM = {10:'TRACE',20:'DEBUG',25:'PROMPT',26:'LLM',30:'INFO',40:'WARN',50:'ERROR',60:'FATAL'};
function normLevel(v){
  if (v==null) return 'UNKNOWN';
  if (typeof v==='number') return LEVEL_MAP_NUM[v]||String(v);
  const s=String(v).toUpperCase();
  return LEVEL_MAP_NUM[Number(s)] || s;
}

function parseWhen(obj){
  const t = obj.time ?? obj.timestamp;
  if (t==null) return null;
  if (typeof t==='number') return new Date(t);
  const d=new Date(String(t));
  return isNaN(d)?null:d;
}

function prettyLine(obj){
  const d=parseWhen(obj);
  const ts = d? d.toISOString() : '';
  const lvl = normLevel(obj.level).padEnd(5,' ');
  const mod = (obj.module || obj.path || obj.name || 'root').slice(0,60).padEnd(60,' ');
  const msg = obj.msg ?? obj.message ?? '';
  const extra = obj.evt ? `  [${obj.evt}${obj.dur_ms?` ${obj.dur_ms}ms`:''}]` : '';
  return `${ts}  ${lvl}  ${mod}  ${msg}${extra}`;
}

function buildFilters({ level, mod, since, until, includes, regex, timeareaCenter, timeareaRadiusSec, filterTerms }) {
  let rx=null; if (regex){ try{rx=new RegExp(regex,'i');}catch{} }
  const sinceDate = since? new Date(since): null;
  const untilDate = until? new Date(until): null;
  const wantLvl = level? normLevel(level): null;

  // timearea : centre + rayon (en secondes)
  let areaStart = null, areaEnd = null;
  if (timeareaCenter && timeareaRadiusSec!=null) {
    const c = new Date(timeareaCenter);
    if (!isNaN(c)) {
      const rMs = Number(timeareaRadiusSec) * 1000;
      areaStart = new Date(c.getTime() - rMs);
      areaEnd   = new Date(c.getTime() + rMs);
    }
  }

  // terms (peuvent être multiples) : match sur msg/path/module/evt/name/attrs stringify
  const terms = Array.isArray(filterTerms) ? filterTerms.filter(Boolean) : (filterTerms ? [filterTerms] : []);

  return { wantLvl, mod, sinceDate, untilDate, includes, rx, areaStart, areaEnd, terms };
}

function objectToSearchString(o) {
  const parts = [];
  if (o.msg!=null) parts.push(String(o.msg));
  if (o.message!=null) parts.push(String(o.message));
  if (o.module!=null) parts.push(String(o.module));
  if (o.path!=null) parts.push(String(o.path));
  if (o.name!=null) parts.push(String(o.name));
  if (o.evt!=null) parts.push(String(o.evt));
  if (o.span!=null) parts.push(String(o.span));
  if (o.attrs!=null) parts.push(safeStringify(o.attrs));
  return parts.join(' | ').toLowerCase();
}

function safeStringify(v){ try{return JSON.stringify(v);}catch{return String(v);} }

function passesAll(obj,f){
  if (!obj || typeof obj!=='object') return false;

  if (f.wantLvl && normLevel(obj.level)!==f.wantLvl) return false;

  if (f.mod){
    const mod = String(obj.module||obj.path||obj.name||'');
    if (mod!==f.mod) return false;
  }

  // since/until
  let d=parseWhen(obj);
  if (f.sinceDate || f.untilDate){
    if (!d) return false;
    if (f.sinceDate && d < f.sinceDate) return false;
    if (f.untilDate && d > f.untilDate) return false;
  }

  // timearea (zone centrée)
  if (f.areaStart || f.areaEnd) {
    if (!d) d = parseWhen(obj);
    if (!d) return false;
    if (f.areaStart && d < f.areaStart) return false;
    if (f.areaEnd && d > f.areaEnd) return false;
  }

  const msg = String(obj.msg ?? obj.message ?? '');
  if (f.includes && !msg.toLowerCase().includes(String(f.includes).toLowerCase())) return false;
  if (f.rx && !f.rx.test(msg)) return false;

  // terms : tous les --filter doivent matcher (AND)
  if (f.terms && f.terms.length) {
    const hay = objectToSearchString(obj); // multi-champs
    for (const t of f.terms) {
      if (!hay.includes(String(t).toLowerCase())) return false;
    }
  }

  return true;
}

function applyFilters(arr, f){ return arr.filter(o=>passesAll(o,f)); }
function safeParse(line){ try{return JSON.parse(line);}catch{return null;} }
function safeParseLines(lines){ const out=[]; for(const l of lines){const o=safeParse(l); if(o) out.push(o);} return out; }

async function getFileSize(file){ const st=await fs.promises.stat(file).catch(()=>null); if(!st) throw new Error(`Log file not found: ${file}`); return st.size; }
async function readAllLines(file){ const data=await fs.promises.readFile(file,'utf8'); const lines=data.split(/\r?\n/).filter(Boolean); return safeParseLines(lines); }

async function tailJsonl(file, approxLines=DEFAULT_LAST_LINES){
  const fd=await fs.promises.open(file,'r');
  try{
    const stat=await fd.stat(); const chunk=64*1024;
    let pos=stat.size; let buffer=''; const lines=[];
    while(pos>0 && lines.length<approxLines){
      const sz=Math.min(chunk,pos); pos-=sz;
      const buf=Buffer.alloc(sz); await fd.read(buf,0,sz,pos);
      buffer = buf.toString('utf8') + buffer;
      let parts=buffer.split(/\r?\n/); buffer=parts.shift();
      for(const p of parts){ if(!p.trim()) continue; const o=safeParse(p); if(o) lines.push(o); }
    }
    if (buffer && buffer.trim()){ const o=safeParse(buffer); if(o) lines.unshift(o); }
    return lines.slice(-approxLines);
  } finally { await fd.close(); }
}

async function streamFilter(file, filters, limit){
  const rl=readline.createInterface({ input: fs.createReadStream(file,{encoding:'utf8'}), crlfDelay:Infinity });
  const out=[];
  for await (const line of rl){
    if (!line.trim()) continue;
    const o=safeParse(line); if(!o) continue;
    if (passesAll(o,filters)){ out.push(o); if (out.length>=limit) break; }
  }
  rl.close(); return out;
}

async function streamEach(file, onObj){
  const rl=readline.createInterface({ input: fs.createReadStream(file,{encoding:'utf8'}), crlfDelay:Infinity });
  for await (const line of rl){ if(!line.trim()) continue; const o=safeParse(line); if(o) onObj(o); }
  rl.close();
}

async function getLast(opts={}){
  const {
    lines=DEFAULT_LAST_LINES, level, module:mod, since, until, includes, regex,
    timeareaCenter, timeareaRadiusSec, filterTerms, pretty=false
  } = opts;

  const filters=buildFilters({level,mod,since,until,includes,regex,timeareaCenter,timeareaRadiusSec,filterTerms});
  const size=await getFileSize(LOG_FILE);

  if (size<=MB(MAX_SAFE_READ_MB)){
    const arr=await readAllLines(LOG_FILE);
    const out=applyFilters(arr.slice(-Math.max(lines,1)),filters);
    return pretty? out.map(prettyLine): out;
  }
  const out=await tailJsonl(LOG_FILE, lines*3);
  const filtered=applyFilters(out,filters).slice(-Math.max(lines,1));
  return pretty? filtered.map(prettyLine): filtered;
}

async function search(opts={}){
  const {
    limit=500, level, module:mod, since, until, includes, regex,
    timeareaCenter, timeareaRadiusSec, filterTerms, pretty=false
  } = opts;

  const filters=buildFilters({level,mod,since,until,includes,regex,timeareaCenter,timeareaRadiusSec,filterTerms});
  const size=await getFileSize(LOG_FILE);
  const res = size<=MB(MAX_SAFE_READ_MB)
    ? applyFilters(await readAllLines(LOG_FILE),filters).slice(-limit)
    : await streamFilter(LOG_FILE,filters,limit);
  return pretty? res.map(prettyLine): res;
}

async function stats(opts={}){
  const {by='level', since, until, level, module:mod, includes, regex, timeareaCenter, timeareaRadiusSec, filterTerms}=opts;
  const filters=buildFilters({level,mod,since,until,includes,regex,timeareaCenter,timeareaRadiusSec,filterTerms});
  const agg={};
  await streamEach(LOG_FILE,(o)=>{
    if(!passesAll(o,filters)) return;
    let key;
    if (by==='day'){ const d=parseWhen(o); if(!d) return; key=d.toISOString().slice(0,10); }
    else if (by==='module'){ key= o.module || o.path || o.name || 'unknown'; }
    else { key= normLevel(o.level); }
    agg[key]=(agg[key]||0)+1;
  });
  return Object.entries(agg).sort((a,b)=>b[1]-a[1]).map(([k,v])=>({[by]:k, count:v}));
}

// --- CLI ---
if (require.main===module){
  (async ()=>{
    try{
      const args=parseArgs(process.argv.slice(2));
      if (args.help) return printHelp();
      if (args.file) setLogFile(args.file);
      // Support for positional filename arguments
      if (args.unknown && args.unknown.length > 0 && !args.file) {
        const possibleFile = args.unknown[0];
        if (possibleFile && !possibleFile.startsWith('-')) {
          setLogFile(possibleFile);
        }
      }

      const common = {
        level: args.level,
        module: args.module,
        since: args.since,
        until: args.until,
        includes: args.includes,
        regex: args.regex,
        timeareaCenter: args.timeareaCenter,
        timeareaRadiusSec: args.timeareaRadiusSec,
        filterTerms: args.filterTerms,
      };

      if (args.stats){
        const res=await stats({by:args.by||'level', ...common});
        return console.log(JSON.stringify(res,null,2));
      }
      if (args.search){
        const res=await search({limit:toInt(args.limit,500), ...common, pretty:!!args.pretty});
        return printResult(res,!!args.pretty);
      }
      const res=await getLast({lines:toInt(args.last,DEFAULT_LAST_LINES), ...common, pretty:!!args.pretty});
      return printResult(res,!!args.pretty);
    }catch(e){ console.error(`[logViewer] Error: ${e.message}`); process.exitCode=1; }
  })();
}

function parseArgs(argv){
  const o={ filterTerms: [] };
  for(let i=0;i<argv.length;i++){
    const a=argv[i], nx=()=> (i+1<argv.length?argv[i+1]:undefined);
    switch(a){
      case '--help': case '-h': o.help=true; break;
      case '--file': o.file=nx(); i++; break;
      case '--last': o.last=nx(); i++; break;
      case '--search': o.search=true; break;
      case '--limit': o.limit=nx(); i++; break;
      case '--level': o.level=nx(); i++; break;
      case '--module': o.module=nx(); i++; break;
      case '--since': o.since=nx(); i++; break;
      case '--until': o.until=nx(); i++; break;
      case '--includes': o.includes=nx(); i++; break;
      case '--regex': o.regex=nx(); i++; break;
      case '--pretty': o.pretty=true; break;
      case '--stats': o.stats=true; break;
      case '--by': o.by=nx(); i++; break;

      // NEW: --timearea <ISO> <seconds>
      case '--timearea': {
        o.timeareaCenter = nx(); i++;
        const radius = nx(); i++;
        o.timeareaRadiusSec = radius != null ? Number(radius) : undefined;
        break;
      }

      // NEW: --filter  (répétable)
      case '--filter': {
        const term = nx(); i++;
        if (term!=null) o.filterTerms.push(term);
        break;
      }

      default: (o.unknown??=[]).push(a);
    }
  }
  if (o.filterTerms.length===0) delete o.filterTerms;
  return o;
}

function printHelp(){
  const bin=`node ${path.relative(process.cwd(), __filename)}`;
  console.log(`
LogViewer (Pino-compatible JSONL)

Usage:
  ${bin} [--file logs/app.log] [--pretty] [--last 200] [filters...]
  ${bin} --search [--limit 500] [filters...]
  ${bin} --stats [--by level|module|day] [filters...]

Time filters:
  --since   2025-09-02T00:00:00Z
  --until   2025-09-02T23:59:59Z
  --timearea <ISO_CENTER> <RADIUS_SECONDS>   # fenêtre centrée

Text filters:
  --includes "keyword in msg"
  --regex "(timeout|ECONNRESET)"
  --filter TERM   # multi-champs (msg, path/module, name, evt, attrs). Répétable. AND.

Other filters:
  --level 30|INFO|ERROR
  --module "Workflow SEO > Génération contenu multi-LLM"

Examples:
  ${bin} --timearea 2025-09-02T23:59:59Z 200 --pretty
  ${bin} --timearea 2025-09-02T12:00:00Z 900 --filter INFO --filter PROMPT --search --pretty
  ${bin} --last 300 --level ERROR --filter "Génération contenu" --pretty
`);}

function printResult(res, pretty){ console.log(pretty? res.join(os.EOL) : JSON.stringify(res,null,2)); }

module.exports = { setLogFile, getLast, search, stats };
