#!/usr/bin/env node

import fs from 'node:fs';
import path from 'node:path';

const dataDir = path.resolve(process.argv[2] || 'data');
const makeAliases = !process.argv.includes('--no-aliases');
const generatedAt = new Date().toISOString();

function readJson(filePath, fallback = null) {
  try {
    return JSON.parse(fs.readFileSync(filePath, 'utf8'));
  } catch {
    return fallback;
  }
}

function listFiles(dir) {
  if (!fs.existsSync(dir)) return [];
  const out = [];
  for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
    const full = path.join(dir, entry.name);
    if (entry.isDirectory()) out.push(...listFiles(full));
    else if (entry.isFile()) out.push(full);
  }
  return out;
}

function safeSlug(input, fallback = 'item') {
  const raw = String(input || '').trim().toLowerCase();
  const ascii = raw
    .replace(/[\u2018\u2019]/g, "'")
    .replace(/[\u201c\u201d]/g, '"')
    .replace(/[^a-z0-9]+/g, '-')
    .replace(/^-+|-+$/g, '')
    .slice(0, 72);
  return ascii || fallback;
}

function publicUrlToFile(url) {
  if (!url || typeof url !== 'string') return null;
  if (!url.startsWith('/api/img/')) return null;
  const parts = url.split('/').filter(Boolean);
  if (parts.length < 4) return null;
  return path.join(dataDir, parts[2], parts.slice(3).join('/'));
}

function extFromUrl(url) {
  const clean = String(url || '').split('?')[0];
  const ext = path.extname(clean);
  return ext || '.png';
}

function pngSize(buffer) {
  if (buffer.length < 24) return null;
  if (buffer.toString('ascii', 1, 4) !== 'PNG') return null;
  return { width: buffer.readUInt32BE(16), height: buffer.readUInt32BE(20) };
}

function jpegSize(buffer) {
  if (buffer.length < 4 || buffer[0] !== 0xff || buffer[1] !== 0xd8) return null;
  let offset = 2;
  while (offset < buffer.length) {
    if (buffer[offset] !== 0xff) return null;
    const marker = buffer[offset + 1];
    const length = buffer.readUInt16BE(offset + 2);
    if ([0xc0, 0xc1, 0xc2, 0xc3].includes(marker)) {
      return { width: buffer.readUInt16BE(offset + 7), height: buffer.readUInt16BE(offset + 5) };
    }
    offset += 2 + length;
  }
  return null;
}

function svgSize(text) {
  const openTag = text.match(/<svg\b[^>]*>/i)?.[0] || '';
  const width = openTag.match(/\bwidth=["']?([0-9.]+)/i)?.[1];
  const height = openTag.match(/\bheight=["']?([0-9.]+)/i)?.[1];
  const viewBox = openTag.match(/\bviewBox=["']\s*[-0-9.]+\s+[-0-9.]+\s+([0-9.]+)\s+([0-9.]+)/i);
  if (width && height) return { width: Number(width), height: Number(height) };
  if (viewBox) return { width: Number(viewBox[1]), height: Number(viewBox[2]) };
  return null;
}

function imageMeta(filePath) {
  if (!filePath || !fs.existsSync(filePath)) return { exists: false };
  const stat = fs.statSync(filePath);
  const ext = path.extname(filePath).toLowerCase();
  const buffer = fs.readFileSync(filePath);
  const size =
    ext === '.png' ? pngSize(buffer)
      : ['.jpg', '.jpeg'].includes(ext) ? jpegSize(buffer)
        : ext === '.svg' ? svgSize(buffer.toString('utf8'))
          : null;
  return {
    exists: true,
    sizeBytes: stat.size,
    mtime: stat.mtime.toISOString(),
    width: size?.width || null,
    height: size?.height || null,
    ratio: size?.width && size?.height ? Number((size.width / size.height).toFixed(4)) : null,
  };
}

function packValues(packs) {
  if (Array.isArray(packs)) return packs;
  if (packs && typeof packs === 'object') return Object.values(packs);
  return [];
}

function fileRecordFromUrl({ session, section, title, id, url, kind, view, templateId, source }) {
  const filePath = publicUrlToFile(url);
  const relativePath = filePath ? path.relative(dataDir, filePath) : null;
  return {
    sessionId: session.id,
    sessionPrompt: session.prompt || '',
    section,
    source,
    id,
    title: title || templateId || view || id,
    kind: kind || null,
    view: view || null,
    templateId: templateId || null,
    url: url || null,
    path: relativePath,
    displayName: `${safeSlug(kind || section, 'asset')}_${safeSlug(templateId || view || title || id, 'asset')}_${safeSlug(id, 'id')}${extFromUrl(url)}`,
    ...imageMeta(filePath),
  };
}

function sessionSlug(session, index) {
  const name = session.characterSpec?.name || session.prompt || session.id;
  return `${String(index + 1).padStart(2, '0')}-${safeSlug(name, session.id)}`;
}

function ensureAlias(asset, sessionAliasDir) {
  if (!asset.path || !asset.exists) return null;
  const section = safeSlug(asset.section || 'asset');
  const aliasDir = path.join(sessionAliasDir, section);
  fs.mkdirSync(aliasDir, { recursive: true });
  const aliasPath = path.join(aliasDir, asset.displayName);
  const target = path.relative(aliasDir, path.join(dataDir, asset.path));

  try {
    const current = fs.lstatSync(aliasPath);
    if (current.isSymbolicLink()) fs.unlinkSync(aliasPath);
    else return { path: path.relative(dataDir, aliasPath), skipped: 'existing non-symlink' };
  } catch {
    // no existing alias
  }

  fs.symlinkSync(target, aliasPath);
  return { path: path.relative(dataDir, aliasPath), target: asset.path };
}

const sessionFiles = fs.existsSync(path.join(dataDir, 'sessions'))
  ? fs.readdirSync(path.join(dataDir, 'sessions')).filter(name => name.endsWith('.json')).sort()
  : [];

const sessions = sessionFiles
  .map(name => ({ file: path.join(dataDir, 'sessions', name), data: readJson(path.join(dataDir, 'sessions', name), null) }))
  .filter(item => item.data?.id);

const allAssets = [];
const sessionSummaries = [];

sessions.forEach(({ file, data: session }, index) => {
  const assets = [];
  const add = asset => {
    if (asset?.url) {
      assets.push(asset);
      allAssets.push(asset);
    }
  };

  for (const upload of session.uploadedImages || []) {
    add(fileRecordFromUrl({
      session,
      section: 'uploads',
      source: 'uploaded_image',
      id: upload.id,
      title: upload.originalFilename || upload.filename || upload.role,
      url: upload.url,
      kind: upload.role,
      view: upload.role,
      templateId: upload.role,
    }));
  }

  for (const image of session.images || []) {
    add(fileRecordFromUrl({
      session,
      section: image.status === 'selected' ? 'selected_candidates' : 'candidates',
      source: image.meta?.provider || 'candidate',
      id: image.id,
      title: image.prompt,
      url: image.url,
      kind: 'candidate',
      view: String(image.meta?.index ?? ''),
      templateId: `candidate_${image.meta?.index ?? image.id}`,
    }));
    if (image.meta?.selectedUrl) {
      add(fileRecordFromUrl({
        session,
        section: 'selected',
        source: 'selected_copy',
        id: `${image.id}_selected`,
        title: image.prompt,
        url: image.meta.selectedUrl,
        kind: 'selected',
        view: String(image.meta?.index ?? ''),
        templateId: `selected_${image.meta?.index ?? image.id}`,
      }));
    }
  }

  if (session.characterSpec?.cleanReferenceImageUrl) {
    add(fileRecordFromUrl({
      session,
      section: 'anchors',
      source: 'clean_reference',
      id: `${session.id}_clean_anchor`,
      title: session.characterSpec.name || 'clean anchor',
      url: session.characterSpec.cleanReferenceImageUrl,
      kind: 'anchor',
      view: 'clean',
      templateId: 'l1_clean_reference',
    }));
  }

  for (const pack of packValues(session.packs)) {
    for (const asset of pack?.assets || []) {
      add(fileRecordFromUrl({
        session,
        section: `pack_${asset.kind || pack.kind || 'unknown'}`,
        source: 'pack_asset',
        id: asset.id || asset.assetId,
        title: asset.title,
        url: asset.url,
        kind: asset.kind || pack.kind,
        view: asset.view,
        templateId: asset.templateId,
      }));
    }
  }

  sessionSummaries.push({
    index: index + 1,
    id: session.id,
    file: path.relative(dataDir, file),
    aliasFolder: sessionSlug(session, index),
    prompt: session.prompt || '',
    inputMode: session.inputMode || null,
    createdAt: session.createdAt || null,
    characterName: session.characterSpec?.name || null,
    uploadedCount: session.uploadedImages?.length || 0,
    candidateCount: session.images?.length || 0,
    packCount: packValues(session.packs).length,
    assetCount: assets.length,
  });
});

const referenced = new Set(allAssets.map(asset => asset.path).filter(Boolean));
const ignored = new Set(['app.db', 'resource-index.json', 'resource-index.md']);
const files = listFiles(dataDir)
  .map(file => path.relative(dataDir, file))
  .filter(file => !file.startsWith(`named${path.sep}`))
  .filter(file => !file.startsWith(`sessions${path.sep}`))
  .filter(file => !ignored.has(file));
const unreferencedFiles = files.filter(file => !referenced.has(file));

let aliases = [];
if (makeAliases) {
  const namedDir = path.join(dataDir, 'named');
  fs.mkdirSync(namedDir, { recursive: true });
  allAssets.forEach(asset => {
    const summary = sessionSummaries.find(item => item.id === asset.sessionId);
    const alias = ensureAlias(asset, path.join(namedDir, summary?.aliasFolder || asset.sessionId));
    if (alias) aliases.push({ ...alias, sessionId: asset.sessionId, assetId: asset.id });
  });
  const unindexedDir = path.join(namedDir, '_unindexed');
  fs.mkdirSync(unindexedDir, { recursive: true });
  for (const file of unreferencedFiles) {
    const source = path.join(dataDir, file);
    const ext = path.extname(file);
    const basename = safeSlug(file.replace(ext, ''), 'unindexed');
    const aliasPath = path.join(unindexedDir, `${basename}${ext}`);
    const target = path.relative(unindexedDir, source);
    try {
      const current = fs.lstatSync(aliasPath);
      if (current.isSymbolicLink()) fs.unlinkSync(aliasPath);
      else {
        aliases.push({ path: path.relative(dataDir, aliasPath), target: file, skipped: 'existing non-symlink' });
        continue;
      }
    } catch {
      // no existing alias
    }
    fs.symlinkSync(target, aliasPath);
    aliases.push({ path: path.relative(dataDir, aliasPath), target: file, section: 'unindexed' });
  }
}

const index = {
  generatedAt,
  dataDir,
  totals: {
    sessions: sessionSummaries.length,
    assets: allAssets.length,
    referencedFiles: referenced.size,
    unreferencedFiles: unreferencedFiles.length,
    aliases: aliases.length,
  },
  sessions: sessionSummaries,
  assets: allAssets,
  unreferencedFiles,
  aliases,
};

fs.writeFileSync(path.join(dataDir, 'resource-index.json'), JSON.stringify(index, null, 2) + '\n');

const md = [
  '# Resource Index',
  '',
  `Generated at: ${generatedAt}`,
  `Data directory: ${dataDir}`,
  '',
  `- Sessions: ${index.totals.sessions}`,
  `- Assets in sessions: ${index.totals.assets}`,
  `- Referenced files: ${index.totals.referencedFiles}`,
  `- Unreferenced files: ${index.totals.unreferencedFiles}`,
  `- Named aliases: ${index.totals.aliases}`,
  '',
  '## Sessions',
  '',
  ...sessionSummaries.flatMap(session => [
    `### ${session.index}. ${session.characterName || session.prompt || session.id}`,
    '',
    `- id: \`${session.id}\``,
    `- mode: \`${session.inputMode || 'unknown'}\``,
    `- alias folder: \`data/named/${session.aliasFolder}/\``,
    `- assets: ${session.assetCount}`,
    `- packs: ${session.packCount}`,
    '',
  ]),
  '## Missing Or Unindexed Files',
  '',
  ...(unreferencedFiles.length ? unreferencedFiles.map(file => `- \`${file}\``) : ['None']),
  '',
].join('\n');

fs.writeFileSync(path.join(dataDir, 'resource-index.md'), md);
console.log(`Resource index written to ${path.join(dataDir, 'resource-index.json')}`);
console.log(`Markdown index written to ${path.join(dataDir, 'resource-index.md')}`);
if (makeAliases) console.log(`Named aliases written under ${path.join(dataDir, 'named')}`);
