/**
 * Meta-Diff — deterministic diff of two ProjectMeta/1.0 JSONs
 *
 * No LLM required. Pure algorithm, runs in milliseconds.
 * Uses VL's structured JSON schema for precise change detection.
 *
 * Input:  oldMeta, newMeta (ProjectMeta/1.0 objects)
 * Output: {
 *   added:    { apps, sections, components, services, tables },
 *   removed:  { apps, sections, components, services, tables },
 *   modified: { apps: [{id, changes}], sections: [...], ... },
 *   affectedFiles: string[],   // files that need regeneration
 *   cascadeImpact: string[],   // indirectly affected files (via cross-refs)
 *   summary: string,           // human-readable summary
 * }
 */

/** Entry point: compare two ProjectMeta objects */
export function computeMetaDiff(oldMeta, newMeta, symbolIndex = null) {
  const result = {
    added:    { apps: [], sections: [], components: [], services: [], tables: [] },
    removed:  { apps: [], sections: [], components: [], services: [], tables: [] },
    modified: { apps: [], sections: [], components: [], services: [], tables: [] },
    affectedFiles: [],
    cascadeImpact: [],
    summary: '',
  };

  const affected = new Set();
  const cascade = new Set();

  // 1. Apps
  diffList(
    oldMeta?.apps || [],
    newMeta?.apps || [],
    'appId',
    result, 'apps',
    item => `Apps/${item.appId}.vx`,
    affected,
    diffAppFields
  );

  // 2. Sections
  diffList(
    oldMeta?.sections || [],
    newMeta?.sections || [],
    'id',
    result, 'sections',
    item => `Sections/${item.id}.sc`,
    affected,
    diffSectionFields
  );

  // 3. Components
  diffList(
    oldMeta?.components || [],
    newMeta?.components || [],
    'id',
    result, 'components',
    item => `ExtComponents/${item.id}.cp`,
    affected,
    diffComponentFields
  );

  // 4. Services
  diffList(
    oldMeta?.services || [],
    newMeta?.services || [],
    'domainId',
    result, 'services',
    item => `Services/${item.domainId}.vs`,
    affected,
    diffServiceFields
  );

  // 5. Tables (inside dataSchema)
  diffList(
    oldMeta?.dataSchema?.tables || [],
    newMeta?.dataSchema?.tables || [],
    'id',
    result, 'tables',
    item => resolveDatabaseFilePath(newMeta, oldMeta, item),
    affected,
    diffTableFields
  );

  result.affectedFiles = [...affected];

  // Cascade impact via SymbolIndex cross-references
  if (symbolIndex?.references) {
    const refs = symbolIndex.references;

    // Changed/removed services → sections that call them
    const changedServiceIds = new Set([
      ...result.modified.services.map(s => s.id),
      ...result.removed.services.map(s => s.id),
    ]);
    for (const domainId of changedServiceIds) {
      refs
        .filter(r => r.type === 'calls_service' && r.toName === domainId)
        .forEach(r => { if (!affected.has(r.from.file)) cascade.add(r.from.file); });
    }

    // Changed/removed components → sections/apps that use them
    const changedCompIds = new Set([
      ...result.modified.components.map(c => c.id),
      ...result.removed.components.map(c => c.id),
    ]);
    for (const compId of changedCompIds) {
      refs
        .filter(r => r.type === 'uses_component' && r.toName === compId)
        .forEach(r => { if (!affected.has(r.from.file)) cascade.add(r.from.file); });
    }
  }

  result.cascadeImpact = [...cascade];
  result.summary = buildSummary(result);
  return result;
}

// ─────────────────────────────────────────────────────────
// Core diff helper
// ─────────────────────────────────────────────────────────

function diffList(oldList, newList, idKey, result, category, toFilePath, affected, diffFieldsFn) {
  const oldMap = new Map(oldList.map(item => [item[idKey], item]));
  const newMap = new Map(newList.map(item => [item[idKey], item]));

  // Added
  for (const [id, item] of newMap) {
    if (!oldMap.has(id)) {
      result.added[category].push({ id });
      affected.add(toFilePath(item));
    }
  }

  // Removed
  for (const [id, item] of oldMap) {
    if (!newMap.has(id)) {
      result.removed[category].push({ id });
      affected.add(toFilePath(item));
    }
  }

  // Modified
  for (const [id, newItem] of newMap) {
    const oldItem = oldMap.get(id);
    if (!oldItem) continue;
    const changes = diffFieldsFn(oldItem, newItem);
    if (changes.length > 0) {
      result.modified[category].push({ id, changes });
      affected.add(toFilePath(newItem));
    }
  }
}

// ─────────────────────────────────────────────────────────
// Field-level diff functions per entity type
// ─────────────────────────────────────────────────────────

function diffAppFields(old, next) {
  const changes = [];
  if (JSON.stringify(old.pages)   !== JSON.stringify(next.pages))   changes.push('pages changed');
  if (JSON.stringify(old.layout)  !== JSON.stringify(next.layout))  changes.push('layout changed');
  if (JSON.stringify(old.routes)  !== JSON.stringify(next.routes))  changes.push('routes changed');
  if (old.defaultPage !== next.defaultPage)                         changes.push(`defaultPage: ${old.defaultPage} → ${next.defaultPage}`);
  return changes;
}

function diffSectionFields(old, next) {
  const changes = [];
  const oldEls = indexBy(old.elements || [], 'id');
  const newEls = indexBy(next.elements || [], 'id');

  for (const [id, el] of Object.entries(newEls)) {
    if (!oldEls[id]) {
      changes.push(`added element: ${id}(${el.type || ''})`);
    } else if (JSON.stringify(oldEls[id]) !== JSON.stringify(el)) {
      changes.push(`modified element: ${id}`);
    }
  }
  for (const id of Object.keys(oldEls)) {
    if (!newEls[id]) changes.push(`removed element: ${id}`);
  }

  if (JSON.stringify(old.publicProps)   !== JSON.stringify(next.publicProps))   changes.push('publicProps changed');
  if (JSON.stringify(old.publicEvents)  !== JSON.stringify(next.publicEvents))  changes.push('publicEvents changed');
  if (old.layout !== next.layout)                                                changes.push(`layout: ${old.layout} → ${next.layout}`);
  return changes;
}

function diffComponentFields(old, next) {
  const changes = [];
  if (JSON.stringify(old.props)  !== JSON.stringify(next.props))  changes.push('props changed');
  if (JSON.stringify(old.events) !== JSON.stringify(next.events)) changes.push('events changed');
  if (JSON.stringify(old.slots)  !== JSON.stringify(next.slots))  changes.push('slots changed');
  return changes;
}

function diffServiceFields(old, next) {
  const changes = [];
  const oldMethods = indexBy(old.methods || [], 'name');
  const newMethods = indexBy(next.methods || [], 'name');

  for (const [name, m] of Object.entries(newMethods)) {
    if (!oldMethods[name]) {
      changes.push(`added method: ${name}`);
    } else {
      const om = oldMethods[name];
      if (JSON.stringify(om.params) !== JSON.stringify(m.params) ||
          JSON.stringify(om.returns) !== JSON.stringify(m.returns)) {
        changes.push(`modified method: ${name} (params/returns changed)`);
      }
    }
  }
  for (const name of Object.keys(oldMethods)) {
    if (!newMethods[name]) changes.push(`removed method: ${name}`);
  }
  return changes;
}

function diffTableFields(old, next) {
  const changes = [];
  const oldCols = indexBy(old.fields || old.columns || [], 'name');
  const newCols = indexBy(next.fields || next.columns || [], 'name');

  for (const [name, col] of Object.entries(newCols)) {
    if (!oldCols[name]) {
      changes.push(`added column: ${name}(${col.type || ''})`);
    } else {
      const oc = oldCols[name];
      if (oc.type !== col.type || JSON.stringify(oc.constraints) !== JSON.stringify(col.constraints)) {
        changes.push(`modified column: ${name} (type/constraints changed)`);
      }
    }
  }
  for (const name of Object.keys(oldCols)) {
    if (!newCols[name]) changes.push(`removed column: ${name}`);
  }
  return changes;
}

// ─────────────────────────────────────────────────────────
// Utilities
// ─────────────────────────────────────────────────────────

function indexBy(arr, key) {
  const m = {};
  for (const item of arr) if (item[key] !== undefined) m[item[key]] = item;
  return m;
}

function resolveDatabaseFilePath(newMeta, oldMeta, item) {
  if (item?.filePath) return item.filePath;
  const projectName = newMeta?.projectName
    || oldMeta?.projectName
    || newMeta?.project?.name
    || oldMeta?.project?.name
    || 'Database';
  return `Database/${projectName}.vdb`;
}

function buildSummary(diff) {
  const parts = [];
  for (const cat of ['apps', 'sections', 'components', 'services', 'tables']) {
    const a = diff.added[cat].length;
    const r = diff.removed[cat].length;
    const m = diff.modified[cat].length;
    if (a + r + m > 0) {
      const tokens = [];
      if (a) tokens.push(`+${a}`);
      if (r) tokens.push(`-${r}`);
      if (m) tokens.push(`~${m}`);
      parts.push(`${cat}(${tokens.join(',')})`);
    }
  }
  if (parts.length === 0) return 'No changes detected';
  const total = diff.affectedFiles.length;
  const cascade = diff.cascadeImpact.length;
  return `Changes: ${parts.join(' | ')} → ${total} file(s) affected${cascade ? `, ${cascade} cascade` : ''}`;
}
