/**
 * Metadata Extractor Bridge — ESM wrapper for vl-metadata-extractor.js (CJS)
 *
 * Searches for the extractor in multiple locations:
 *   1. <workDir>/vl-metadata-extractor.js
 *   2. <workDir>/.vl-code/vl-metadata-extractor.js
 *   3. ../vl-metadata-extractor.js (sibling to project)
 *
 * Provides:
 *   - extractFromFileTree(fileTree) → full ProjectMeta/1.0
 *   - extractSingleFile(filePath, content) → parsed module entry
 *   - mergeIntoMeta(meta, type, entry) → merge single-file result into existing meta
 *   - validateMeta(meta) → consistency checks (§5 of spec)
 *   - buildGraph(meta) → visualization graph
 */
import { createRequire } from 'module';
import path from 'path';
import fs from 'fs';
import vm from 'vm';
import { fileURLToPath } from 'url';

const __dirname = path.dirname(fileURLToPath(import.meta.url));

let _extractor = null;
let _loadedPath = null; // track which path succeeded

function asArray(value) {
  return Array.isArray(value) ? value : [];
}

function compact(value) {
  return value.filter(Boolean);
}

function normalizeFilePath(filePath, fallbackDir, fallbackId, ext) {
  if (filePath) return filePath;
  if (!fallbackId) return '';
  return `${fallbackDir}/${fallbackId}.${ext}`;
}

function normalizeServiceId(ref, domainId = '') {
  if (!ref) return null;
  if (typeof ref === 'string') return ref;
  if (typeof ref === 'object') {
    if (typeof ref.serviceId === 'string') return ref.serviceId;
    const name = ref.id || ref.name || ref.serviceName || ref.method;
    if (domainId && name) return `${domainId}.${name}`;
    if (ref.domainId && name) return `${ref.domainId}.${name}`;
    if (ref.domainId) return ref.domainId;
  }
  return null;
}

function normalizeMethod(method = {}, domainId = '') {
  const serviceId = normalizeServiceId(method, domainId);
  const id = method.id
    || method.name
    || method.serviceName
    || (typeof serviceId === 'string' && serviceId.includes('.') ? serviceId.split('.').pop() : serviceId)
    || null;
  if (!id) return null;
  return {
    id,
    serviceId: serviceId || (domainId ? `${domainId}.${id}` : id),
    type: method.type || method.kind || null,
    params: asArray(method.params || method.parameters),
    returns: method.returns || method.output || method.out || null,
    expose: method.expose || null,
    sig: method.sig || null,
  };
}

function normalizeVirtualTable(vt = {}) {
  const id = vt.id || vt.name || vt.vTableId || vt.instanceId || null;
  if (!id) return null;
  return {
    id,
    source: vt.source || vt.sourceTable || vt.sourceEntity || '',
    fields: compact(asArray(vt.fields).map((field) => {
      if (typeof field === 'string') return field;
      return field?.name || field?.field || field?.sourceField || null;
    })),
    extraSpecs: vt.extraSpecs || {},
  };
}

function normalizeField(field = {}) {
  if (typeof field === 'string') return { name: field, type: 'STRING' };
  const name = field.name || field.field || field.id || null;
  if (!name) return null;
  return {
    name,
    type: field.type || 'STRING',
    notNull: !!field.notNull,
    default: field.default ?? null,
    enumRef: field.enumRef || null,
    sourceField: field.sourceField || null,
  };
}

function normalizeIndex(index = {}) {
  if (!index) return null;
  const name = index.name || index.id || null;
  if (!name) return null;
  return {
    name,
    type: index.type || 'NORMAL',
    fields: asArray(index.fields),
  };
}

function normalizeTable(table = {}) {
  const id = table.id || table.name || table.tableName || table.entityId || null;
  if (!id) return null;
  return {
    id,
    name: id,
    filePath: table.filePath || '',
    fields: compact(asArray(table.fields).map(normalizeField)),
    indexes: compact(asArray(table.indexes).map(normalizeIndex)),
    seedData: table.seedData || null,
  };
}

function normalizeRelation(relation = {}) {
  if (!relation) return null;
  if (typeof relation === 'string') return { id: relation };
  return {
    id: relation.id || relation.name || null,
    from: relation.from || null,
    to: relation.to || null,
    cardinality: relation.cardinality || relation.type || null,
  };
}

function normalizeComponent(component = {}) {
  const id = component.id || component.componentId || component.name || null;
  if (!id) return null;
  return {
    id,
    filePath: normalizeFilePath(component.filePath, 'ExtComponents', id, 'cp'),
    vlVersion: component.vlVersion || null,
    previewSize: component.previewSize || null,
    publicProps: asArray(component.publicProps || component.props),
    publicEvents: asArray(component.publicEvents || component.events),
    derivedVars: asArray(component.derivedVars),
    interactiveElements: asArray(component.interactiveElements),
    internalMethods: asArray(component.internalMethods || component.methods),
    pipeFuncs: asArray(component.pipeFuncs),
    description: component.description || '',
  };
}

function normalizeSection(section = {}) {
  const id = section.id || section.sectionId || section.name || null;
  if (!id) return null;
  const consumesServices = new Set();
  for (const ref of asArray(section.consumesServices || section.servicesUsed || section.services || section.serviceDomains)) {
    if (typeof ref === 'string') {
      consumesServices.add(ref);
      continue;
    }
    if (ref?.domainId && Array.isArray(ref.services) && ref.services.length > 0) {
      for (const svc of ref.services) {
        const serviceId = normalizeServiceId(svc, ref.domainId);
        if (serviceId) consumesServices.add(serviceId);
      }
      continue;
    }
    const direct = normalizeServiceId(ref);
    if (direct) consumesServices.add(direct);
  }

  const usesComponents = new Set();
  for (const ref of asArray(section.usesComponents || section.componentRefs || section.components)) {
    if (typeof ref === 'string') {
      usesComponents.add(ref);
      continue;
    }
    const componentId = ref?.componentId || ref?.id || ref?.name || null;
    if (componentId) usesComponents.add(componentId);
  }

  return {
    id,
    filePath: normalizeFilePath(section.filePath, 'Sections', id, 'sc'),
    vlVersion: section.vlVersion || null,
    previewSize: section.previewSize || null,
    publicProps: asArray(section.publicProps || section.props),
    publicEvents: asArray(section.publicEvents || section.events),
    publicMethods: asArray(section.publicMethods),
    globalVars: asArray(section.globalVars),
    derivedVars: asArray(section.derivedVars),
    consumesServices: [...consumesServices],
    usesComponents: [...usesComponents],
    interactiveElements: asArray(section.interactiveElements),
    internalMethods: asArray(section.internalMethods || section.methods),
    pipeFuncs: asArray(section.pipeFuncs),
    isNavSection: !!section.isNavSection,
    navMenuItems: asArray(section.navMenuItems),
    navItemInstanceId: section.navItemInstanceId || null,
    keyStates: section.keyStates || {},
    description: section.description || '',
  };
}

function normalizeApp(app = {}) {
  const id = app.id || app.appId || app.name || null;
  if (!id) return null;
  const pages = asArray(app.pages).map((page, index) => {
    const sectionRefs = asArray(page.sectionRefs).map((ref) => ({
      sectionId: ref?.sectionId || ref?.id || ref?.name || ref?.ref || null,
      instanceId: ref?.instanceId || null,
      layoutProps: ref?.layoutProps || {},
    })).filter((ref) => ref.sectionId);
    const sections = compact([
      ...asArray(page.sections || page.sectionIds).map((ref) => typeof ref === 'string' ? ref : ref?.sectionId || ref?.id || ref?.name || null),
      ...sectionRefs.map((ref) => ref.sectionId),
    ]);
    const componentRefs = asArray(page.componentRefs).map((ref) => ({
      componentId: typeof ref === 'string' ? ref : ref?.componentId || ref?.id || ref?.name || null,
      instanceId: typeof ref === 'string' ? null : ref?.instanceId || null,
    })).filter((ref) => ref.componentId);
    return {
      id: page.id || page.pageId || page.name || page.route || `${id}_page_${index + 1}`,
      route: page.route || '',
      sections,
      sectionRefs,
      componentRefs,
      layout: asArray(page.layout),
    };
  });
  const routeMap = app.routeMap || Object.fromEntries(
    pages.filter((page) => page.route && page.sections[0]).map((page) => [page.route, page.sections[0]])
  );
  return {
    id,
    filePath: normalizeFilePath(app.filePath, 'Apps', id, 'vx'),
    vlVersion: app.vlVersion || null,
    device: app.device || app.sysConfig?.deviceTarget || null,
    resolution: app.resolution || app.sysConfig?.screenResolution || null,
    description: app.description || '',
    globalVars: asArray(app.globalVars),
    pages,
    routeMap,
    homeRoute: app.homeRoute || Object.keys(routeMap)[0] || '',
    wiring: asArray(app.wiring || app.eventBindings),
    navSectionInstanceId: app.navSectionInstanceId || null,
    navEventName: app.navEventName || null,
  };
}

function normalizeService(service = {}) {
  const domainId = service.domainId || service.id || service.name || null;
  if (!domainId) return null;
  return {
    domainId,
    filePath: normalizeFilePath(service.filePath, 'Services', domainId, 'vs'),
    vlVersion: service.vlVersion || null,
    envVars: asArray(service.envVars),
    methods: compact(asArray(service.methods || service.services).map((method) => normalizeMethod(method, domainId))),
    virtualTables: compact(asArray(service.virtualTables).map(normalizeVirtualTable)),
    transactions: asArray(service.transactions),
    backendComponents: asArray(service.backendComponents),
  };
}

function normalizeTheme(theme = {}) {
  if (!theme || typeof theme !== 'object') return null;
  const id = theme.id || theme.name || theme.themeId || 'Theme';
  const meta = {
    ...(theme.meta || {}),
  };
  if (!meta.mode && theme.mode) meta.mode = theme.mode;
  if (!meta.version && theme.version) meta.version = theme.version;
  if (!meta.styleSpaceVersion && theme.styleSpaceVersion) meta.styleSpaceVersion = theme.styleSpaceVersion;
  if (!meta.base_theme && (theme.base_theme || theme.baseTheme)) meta.base_theme = theme.base_theme || theme.baseTheme;
  if (!meta.profile && theme.profile) meta.profile = theme.profile;
  return {
    id,
    filePath: normalizeFilePath(theme.filePath, 'Theme', id, 'vth'),
    vlVersion: theme.vlVersion || null,
    name: theme.name || id,
    meta,
    rootTag: theme.rootTag || null,
    slots: theme.slots || theme.pointSlotValues || theme.pointSlots || {},
    designTokens: asArray(theme.designTokens),
    componentVariants: asArray(theme.componentVariants),
    bindingRules: asArray(theme.bindingRules),
    overrides: asArray(theme.overrides),
  };
}

function normalizeDataSchema(raw = {}) {
  const source = raw?.tables ? raw : { tables: raw?.tables || [], relations: raw?.relations || [] };
  return {
    tables: compact(asArray(source.tables).map(normalizeTable)),
    relations: compact(asArray(source.relations).map(normalizeRelation)),
  };
}

export function normalizeProjectMeta(rawMeta = {}) {
  if (!rawMeta || typeof rawMeta !== 'object') return null;
  const apps = compact(asArray(rawMeta.apps).map(normalizeApp));
  const sections = compact(asArray(rawMeta.sections).map(normalizeSection));
  const components = compact(asArray(rawMeta.components).map(normalizeComponent));
  const services = compact(asArray(rawMeta.services || rawMeta.serviceDomains).map(normalizeService));
  const dataSchema = normalizeDataSchema(rawMeta.dataSchema || rawMeta.database || {});
  const theme = normalizeTheme(rawMeta.theme);
  const fileManifest = asArray(rawMeta.fileManifest || rawMeta.project?.fileManifest);
  const vlVersion = rawMeta.vlVersion
    || rawMeta.project?.vlVersion
    || rawMeta.config?.vlVersion
    || apps.find((app) => app.vlVersion)?.vlVersion
    || sections.find((section) => section.vlVersion)?.vlVersion
    || components.find((component) => component.vlVersion)?.vlVersion
    || services.find((service) => service.vlVersion)?.vlVersion
    || theme?.vlVersion
    || null;
  const config = {
    ...(rawMeta.config || {}),
  };
  if (!config.deviceTarget) {
    config.deviceTarget = apps.find((app) => app.device)?.device || null;
  }
  if (!config.screenResolution) {
    config.screenResolution = apps.find((app) => app.resolution)?.resolution || null;
  }

  return {
    $schema: rawMeta.$schema || 'VL-ProjectMeta/3.0',
    projectName: rawMeta.projectName || rawMeta.project?.name || rawMeta.project?.projectName || '',
    projectDescription: rawMeta.projectDescription || rawMeta.description || '',
    vlVersion,
    config,
    fileManifest,
    valueDomains: rawMeta.valueDomains || {},
    apps,
    sections,
    components,
    services,
    dataSchema,
    theme,
    dependencyGraph: rawMeta.dependencyGraph || null,
  };
}

/**
 * Load a CJS file reliably regardless of package "type": "module".
 * Uses vm.Script to execute the file in a fresh CJS context,
 * bypassing the ESM loader that would otherwise return an empty namespace.
 */
function requireCJS(filePath) {
  const src = fs.readFileSync(filePath, 'utf-8');
  const mod = { exports: {} };
  const req = createRequire(filePath);
  const wrapper = `(function(module, exports, require, __filename, __dirname) { ${src} })`;
  const fn = vm.runInThisContext(wrapper, { filename: filePath });
  fn(mod, mod.exports, req, filePath, path.dirname(filePath));
  return mod.exports;
}

/**
 * Load the VLMetadataExtractor CJS module.
 * Returns the extractor object or null if not found.
 */
export function getExtractor(workDir) {
  if (_extractor) return _extractor;

  const searchPaths = [];

  if (workDir) {
    searchPaths.push(
      path.join(workDir, 'vl-metadata-extractor.js'),
      path.join(workDir, '.vl-code', 'vl-metadata-extractor.js'),
      path.resolve(workDir, '..', 'vl-metadata-extractor.js'),
    );
  }

  // Also check relative to this source file
  searchPaths.push(
    path.resolve(__dirname, '..', '..', '..', 'vl-metadata-extractor.js'),
    path.resolve(__dirname, '..', '..', 'vl-metadata-extractor.js'),
  );

  for (const p of searchPaths) {
    if (!fs.existsSync(p)) continue;
    try {
      _extractor = requireCJS(p);
      _loadedPath = p;
      return _extractor;
    } catch { continue; }
  }

  return null; // not cached — will retry next call
}

/**
 * Extract full ProjectMeta/1.0 from a fileTree map.
 * @param {Object} fileTree - { 'Apps/X.vx': '...content...', ... }
 * @param {string} [workDir] - working directory for extractor search
 * @returns {Object|null} ProjectMeta/1.0 object or null
 */
export function extractFromFileTree(fileTree, workDir) {
  const ext = getExtractor(workDir);
  if (!ext?.extract) return null;
  return normalizeProjectMeta(ext.extract(fileTree));
}

/**
 * Parse a single VL file into its ProjectMeta entry.
 * @returns {{ type: string, result: Object }|null}
 */
export function extractSingleFile(filePath, content, workDir) {
  const ext = getExtractor(workDir);
  if (!ext?.parsers) return null;

  const fileExt = path.extname(filePath).substring(1).toLowerCase();
  const parserMap = {
    vx: { parser: ext.parsers.parseVX, metaKey: 'apps' },
    sc: { parser: ext.parsers.parseSC, metaKey: 'sections' },
    cp: { parser: ext.parsers.parseCP, metaKey: 'components' },
    vs: { parser: ext.parsers.parseVS, metaKey: 'services' },
    vdb: { parser: ext.parsers.parseVDB, metaKey: 'dataSchema' },
    vth: { parser: ext.parsers.parseVTH, metaKey: 'theme' },
  };

  const entry = parserMap[fileExt];
  if (!entry) return null;

  try {
    const result = entry.parser(content, filePath);
    let normalized = null;
    if (fileExt === 'vdb') {
      normalized = normalizeProjectMeta({ dataSchema: result }).dataSchema;
    } else if (fileExt === 'vth') {
      normalized = normalizeProjectMeta({ theme: result }).theme;
    } else if (fileExt === 'vs') {
      normalized = normalizeProjectMeta({ services: [result] }).services[0] || null;
    } else if (fileExt === 'sc') {
      normalized = normalizeProjectMeta({ sections: [result] }).sections[0] || null;
    } else if (fileExt === 'cp') {
      normalized = normalizeProjectMeta({ components: [result] }).components[0] || null;
    } else if (fileExt === 'vx') {
      normalized = normalizeProjectMeta({ apps: [result] }).apps[0] || null;
    }
    return { type: fileExt, metaKey: entry.metaKey, result: normalized };
  } catch {
    return null;
  }
}

/**
 * Merge a single-file extraction result into existing ProjectMeta.
 * Replaces the matching entry by ID or appends if new.
 */
export function mergeIntoMeta(meta, extracted) {
  if (!meta || !extracted) return meta;
  const normalizedMeta = normalizeProjectMeta(meta);
  Object.keys(meta).forEach((key) => delete meta[key]);
  Object.assign(meta, normalizedMeta);

  const { type, metaKey, result } = extracted;
  if (!result) return meta;

  if (type === 'vdb') {
    // Database replaces the entire dataSchema
    meta.dataSchema = normalizeDataSchema(result);
    return meta;
  }

  if (type === 'vth') {
    meta.theme = normalizeTheme(result);
    return meta;
  }

  // For array-based entries (apps, sections, components, services)
  const arr = meta[metaKey];
  if (!Array.isArray(arr)) return meta;

  const idField = type === 'vs' ? 'domainId' : 'id';
  const id = result[idField];
  const idx = arr.findIndex(item => item[idField] === id);

  if (idx >= 0) {
    arr[idx] = result; // Replace existing
  } else {
    arr.push(result); // Append new
  }

  return meta;
}

/**
 * Validate ProjectMeta consistency (§5 of spec).
 * Returns { valid: boolean, issues: string[] }
 */
export function validateMeta(meta) {
  const normalized = normalizeProjectMeta(meta);
  const issues = [];
  if (!normalized) return { valid: false, issues: ['No ProjectMeta provided'] };

  // §5.1 ID Chain Integrity
  const tableIds = new Set((normalized.dataSchema?.tables || []).map(t => t.id));
  const serviceIds = new Set();
  const componentIds = new Set((normalized.components || []).map(c => c.id));

  for (const svc of (normalized.services || [])) {
    for (const m of (svc.methods || [])) {
      serviceIds.add(m.serviceId || `${svc.domainId}.${m.id}`);
    }
    for (const vt of (svc.virtualTables || [])) {
      if (!tableIds.has(vt.source)) {
        issues.push(`Service ${svc.domainId}: VirtualTable "${vt.id}" references unknown table "${vt.source}"`);
      }
    }
  }

  for (const sec of (normalized.sections || [])) {
    for (const consumed of (sec.consumesServices || [])) {
      if (!serviceIds.has(consumed)) {
        issues.push(`Section ${sec.id}: consumesServices references unknown service "${consumed}"`);
      }
    }
    for (const comp of (sec.usesComponents || [])) {
      if (!componentIds.has(comp)) {
        issues.push(`Section ${sec.id}: usesComponents references unknown component "${comp}"`);
      }
    }
  }

  // §5.3 Wiring Integrity — check app layout refs
  for (const app of (normalized.apps || [])) {
    const layoutRefs = new Set();
    const collectRefs = (nodes) => {
      for (const n of (nodes || [])) {
        if (n.as) layoutRefs.add(n.as);
        if (n.children) collectRefs(n.children);
      }
    };
    for (const page of (app.pages || [])) {
      collectRefs(page.layout);
    }
    for (const w of (app.wiring || [])) {
      const instanceId = w.on?.split('.')[0];
      if (instanceId && !layoutRefs.has(instanceId)) {
        issues.push(`App ${app.id}: wiring references unknown instance "${instanceId}"`);
      }
    }
  }

  // §5.4 Enum References
  const enumNames = new Set((normalized.valueDomains?.enums || []).map(e => e.name));
  for (const table of (normalized.dataSchema?.tables || [])) {
    for (const field of (table.fields || [])) {
      if (field.enumRef && !enumNames.has(field.enumRef)) {
        issues.push(`Table ${table.id}: field "${field.name}" references unknown enum "${field.enumRef}"`);
      }
    }
  }

  return { valid: issues.length === 0, issues };
}

/**
 * Build visualization graph from ProjectMeta.
 */
export function buildMetaGraph(meta, workDir) {
  const ext = getExtractor(workDir);
  if (!ext?.buildGraph) return null;
  return ext.buildGraph(meta);
}
