// engine/note-link-backfill.js
//
// One-time repair for work-item note links that broke BEFORE the live
// rewrite landed (W-mq1j85cj00055a8f extended to _artifacts.notes / artifacts).
//
// Before the rewriter covered completion-report artifact links, any inbox note
// that the engine auto-archived (or that was promoted to KB) left the work
// item's link pointing at the now-gone `notes/inbox/<name>` path — a dangling
// pill in the dashboard. This module scans existing work items, finds links
// whose inbox file is GONE, discovers where the note actually went (the flat
// `notes/archive/` dir or a `knowledge/<cat>/` dir), and repoints the link —
// re-encoding the `_artifacts.notes[].file` token to the renderer's
// `archive:<base>` / `kb:<cat>/<file>` form so the pill resolves again.
//
// It only ever touches links that are PROVABLY broken (inbox file missing) AND
// resolvable (a matching archive/KB file exists). Valid links, already-relocated
// tokens, and notes with no discoverable destination (e.g. merged into the flat
// notes.md) are left untouched. Idempotent: a second pass finds nothing because
// the repaired links no longer reference `notes/inbox/`.
//
// Wired as an idempotent engine-boot sweep (engine/cli.js, alongside the other
// one-shot boot migrations). It runs through shared.mutateWorkItems — the same
// write path the live rewriter uses — so the canonical SQL store and its JSON
// mirror stay consistent, and it's a no-op once every link is healed.

'use strict';

const fs = require('fs');
const path = require('path');

const INBOX_REF_RE = /(?:^|\/)notes\/inbox\/([^/?#]+)/;

// Extract the inbox basename from a `…/notes/inbox/<name>` string (tolerating a
// trailing ?query / #fragment), or null if the string isn't an inbox ref.
function inboxNameFromRef(str) {
  if (typeof str !== 'string') return null;
  const m = str.match(INBOX_REF_RE);
  return m ? m[1] : null;
}

// Build a resolver: inbox basename → { newLocation, fileToken } | null.
//   - null when the note is STILL in the inbox (link isn't broken), or when no
//     destination can be discovered (note merged into notes.md, or deleted).
//   - newLocation is the MINIONS_DIR-relative path; fileToken is the prefixed
//     basename the work-item renderer resolves for note pills.
// The archive dir is flat: consolidation writes `${YYYY-MM-DD}-${name}` (with an
// optional uniquePath `-N` collision suffix). We index each archived file by the
// basename it derived from (date prefix stripped) and by its literal name.
function buildNoteResolver(dirs) {
  const inboxDir = dirs.inboxDir;
  const archiveDir = dirs.archiveDir;
  const knowledgeDir = dirs.knowledgeDir;
  const kbCategories = Array.isArray(dirs.kbCategories) ? dirs.kbCategories : [];
  // Reuse the live rewriter's encoder so the backfill and the live path emit
  // byte-identical pill tokens (one source for the archive:/kb: grammar).
  const tokenFor = require('./shared')._artifactNoteFileToken;

  const archiveByBase = new Map();
  let archiveFiles = [];
  try { archiveFiles = fs.readdirSync(archiveDir); } catch { archiveFiles = []; }
  for (const f of archiveFiles) {
    const origin = f.replace(/^\d{4}-\d{2}-\d{2}-/, '');
    for (const key of new Set([origin, f])) {
      const prev = archiveByBase.get(key);
      // Lexicographic max keeps the newest date-prefixed file for a given note.
      if (!prev || f > prev) archiveByBase.set(key, f);
    }
  }

  const kbByBase = new Map();
  for (const cat of kbCategories) {
    let files = [];
    try { files = fs.readdirSync(path.join(knowledgeDir, cat)); } catch { files = []; }
    for (const f of files) if (!kbByBase.has(f)) kbByBase.set(f, cat + '/' + f);
  }

  return function resolve(baseName) {
    if (!baseName) return null;
    try { if (fs.existsSync(path.join(inboxDir, baseName))) return null; } catch { /* treat as gone */ }
    const arch = archiveByBase.get(baseName);
    if (arch) { const nl = 'notes/archive/' + arch; return { newLocation: nl, fileToken: tokenFor(nl) }; }
    const kb = kbByBase.get(baseName);
    if (kb) { const nl = 'knowledge/' + kb; return { newLocation: nl, fileToken: tokenFor(nl) }; }
    return null;
  };
}

// Repoint every broken inbox note link on one work item in place. `resolve` is a
// buildNoteResolver() result (returns null for non-broken / unresolvable). Mirrors
// the link shapes shared.rewriteInboxRefsAcrossProjects handles: references[],
// _artifacts.notes[] (string + object), and artifacts[].path. Returns the count
// of links repointed.
function repointItemNoteLinks(item, resolve) {
  if (!item || typeof item !== 'object') return 0;
  let fixed = 0;

  if (Array.isArray(item.references)) {
    for (let i = 0; i < item.references.length; i++) {
      const ref = item.references[i];
      if (typeof ref === 'string') {
        const name = inboxNameFromRef(ref);
        if (name) { const d = resolve(name); if (d) { item.references[i] = d.newLocation; fixed++; } }
      } else if (ref && typeof ref === 'object') {
        for (const key of ['url', 'path', 'href']) {
          const name = inboxNameFromRef(ref[key]);
          if (name) { const d = resolve(name); if (d) { ref[key] = d.newLocation; fixed++; break; } }
        }
      }
    }
  }

  const notes = item._artifacts && item._artifacts.notes;
  if (Array.isArray(notes)) {
    for (let i = 0; i < notes.length; i++) {
      const n = notes[i];
      if (typeof n === 'string') {
        // Bare basename = an inbox pill. Skip tokens already relocated.
        if (n.startsWith('archive:') || n.startsWith('kb:')) continue;
        const d = resolve(n);
        if (d && d.fileToken) { notes[i] = d.fileToken; fixed++; }
      } else if (n && typeof n === 'object') {
        let name = inboxNameFromRef(n.path);
        if (!name && typeof n.file === 'string'
            && !n.file.startsWith('archive:') && !n.file.startsWith('kb:')) {
          name = n.file;
        }
        if (name) {
          const d = resolve(name);
          if (d) {
            if (typeof n.path === 'string') n.path = d.newLocation;
            if (d.fileToken && typeof n.file === 'string') n.file = d.fileToken;
            fixed++;
          }
        }
      }
    }
  }

  if (Array.isArray(item.artifacts)) {
    for (const a of item.artifacts) {
      if (a && typeof a === 'object') {
        const name = inboxNameFromRef(a.path);
        if (name) { const d = resolve(name); if (d) { a.path = d.newLocation; fixed++; } }
      }
    }
  }

  return fixed;
}

// Repair broken note links across every work-items file (each project's plus
// the central one), routed through shared.mutateWorkItems so the canonical SQL
// store AND its JSON mirror stay consistent — the same write path the live
// rewriter uses. Idempotent: only touches links that are provably broken AND
// resolvable, so once healed a re-run writes nothing (skipWriteIfUnchanged).
// Safe to call on every engine boot. Per-file try/catch isolates a bad scope.
function runNoteLinkBackfill(opts = {}) {
  const shared = require('./shared');
  const minionsDir = opts.minionsDir || shared.MINIONS_DIR;
  const resolve = buildNoteResolver({
    inboxDir: path.join(minionsDir, 'notes', 'inbox'),
    archiveDir: path.join(minionsDir, 'notes', 'archive'),
    knowledgeDir: path.join(minionsDir, 'knowledge'),
    kbCategories: shared.KB_CATEGORIES,
  });

  let config = opts.config;
  if (!config) {
    try { config = JSON.parse(fs.readFileSync(path.join(minionsDir, 'config.json'), 'utf8')); }
    catch { config = {}; }
  }
  const projects = Array.isArray(config.projects) ? config.projects : [];
  const candidates = [];
  for (const p of projects) {
    if (p && p.name) candidates.push(path.join(minionsDir, 'projects', p.name, 'work-items.json'));
  }
  candidates.push(path.join(minionsDir, 'work-items.json'));

  let scanned = 0, fixedItems = 0, fixedLinks = 0;
  for (const wiPath of candidates) {
    try {
      shared.mutateWorkItems(wiPath, (items) => {
        if (!Array.isArray(items)) return items;
        for (const item of items) {
          if (!item) continue;
          scanned++;
          try {
            const n = repointItemNoteLinks(item, resolve);
            if (n > 0) { fixedItems++; fixedLinks += n; }
          } catch { /* skip unfixable item */ }
        }
        return items;
      });
    } catch { /* per-file isolation — one bad scope can't block the others */ }
  }

  return { scanned, fixedItems, fixedLinks };
}

module.exports = {
  inboxNameFromRef,
  buildNoteResolver,
  repointItemNoteLinks,
  runNoteLinkBackfill,
};
