#!/usr/bin/env node
/*
 * gen-data.mjs — single source of truth: the LibrePortal repo.
 *
 * Reads every containers/<app>/<app>.config METADATA block and emits
 *   src/_data/apps.json        (array of apps, used by the app grid)
 *   src/_data/categories.json  (categories present, with friendly names + counts)
 *
 * Run via `npm run data` (also runs automatically before `npm run build`).
 * Add an app to the repo -> rebuild -> the website updates itself.
 */
import { readdirSync, readFileSync, existsSync, writeFileSync, mkdirSync, statSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, join, resolve } from 'node:path';

const here = dirname(fileURLToPath(import.meta.url));
const repoRoot = resolve(here, '..', '..');           // site/scripts -> repo root
const containersDir = join(repoRoot, 'containers');
const dataDir = join(here, '..', 'src', '_data');
const iconsDir = join(here, '..', 'src', 'assets', 'apps');

// Only these metadata fields are pulled from each .config. LONG_DESCRIPTION must
// precede DESCRIPTION in the alternation so it wins the match.
const FIELD_RE = /^CFG_.+?_(LONG_DESCRIPTION|DESCRIPTION|CATEGORY|TITLE|URL)\s*=\s*"?(.*?)"?\s*$/;

const CATEGORY_NAMES = {
  recommended: 'Recommended', communication: 'Communication', development: 'Development',
  knowledge: 'Knowledge', media: 'Media', monitoring: 'Monitoring', storage: 'Storage',
  network: 'Network', security: 'Security', productivity: 'Productivity', finance: 'Finance',
  ai: 'AI', utilities: 'Utilities', privacy: 'Privacy', social: 'Social', system: 'System',
};
const titleCase = (s) => s.replace(/[-_]/g, ' ').replace(/\b\w/g, (m) => m.toUpperCase());

function parseConfig(text) {
  const out = {};
  for (const raw of text.split(/\r?\n/)) {
    const m = FIELD_RE.exec(raw.trim());
    if (m && out[m[1]] === undefined) out[m[1]] = m[2];
  }
  return out;
}

const apps = [];
for (const slug of readdirSync(containersDir)) {
  const dir = join(containersDir, slug);
  if (!statSync(dir).isDirectory()) continue;
  const cfg = join(dir, `${slug}.config`);
  if (!existsSync(cfg)) continue;
  const meta = parseConfig(readFileSync(cfg, 'utf8'));
  if (!meta.TITLE) continue; // skip anything without a display title (infra without metadata)
  const cats = (meta.CATEGORY || 'utilities').split(',').map((s) => s.trim()).filter(Boolean);
  apps.push({
    slug,
    title: meta.TITLE,
    category: cats[0],     // primary category
    categories: cats,      // an app can belong to several
    description: meta.DESCRIPTION || '',
    longDescription: meta.LONG_DESCRIPTION || meta.DESCRIPTION || '',
    url: meta.URL || '',
    icon: existsSync(join(iconsDir, `${slug}.svg`)) ? `assets/apps/${slug}.svg` : 'assets/apps/default.svg',
  });
}
apps.sort((a, b) => a.title.localeCompare(b.title));

const catMap = new Map();
for (const a of apps) {
  for (const cat of a.categories) {
    const c = catMap.get(cat) || { id: cat, name: CATEGORY_NAMES[cat] || titleCase(cat), count: 0 };
    c.count++;
    catMap.set(cat, c);
  }
}
const categories = [...catMap.values()].sort((a, b) => b.count - a.count || a.name.localeCompare(b.name));

mkdirSync(dataDir, { recursive: true });
writeFileSync(join(dataDir, 'apps.json'), JSON.stringify(apps, null, 2) + '\n');
writeFileSync(join(dataDir, 'categories.json'), JSON.stringify(categories, null, 2) + '\n');
console.log(`✓ ${apps.length} apps across ${categories.length} categories → src/_data/{apps,categories}.json`);
