'use strict';

/**
 * Database repair and integrity tools.
 * Scans for orphaned records, verifies constraints, can rebuild indexes.
 */

const fs = require('node:fs');
const brain = require('../brain');
const { collectIngestRecords } = require('../sources/base');
const { ensureBuiltinSourceAdapters } = require('../sources/builtin');
const sourceRegistry = require('../sources/registry');
const { RECORD_TYPES } = require('../sources/record-types');
const { rebuildSourceIndex } = require('../memory/source-indexer');

/**
 * Scan database for integrity issues.
 * @returns {object} { issues: Array<{type, description, ids}>, stats }
 */
function scanIntegrity() {
  const db = brain.getDb();
  const issues = [];

  // 1. Orphaned knowledge: references memories that don't exist
  try {
    const orphaned = db.prepare(`
      SELECT k.id, k.source_memory_ids FROM knowledge k
      WHERE k.source_memory_ids IS NOT NULL AND k.source_memory_ids != '[]'
      AND k.source_memory_ids != 'null'
    `).all();
    for (const k of orphaned) {
      try {
        const memIds = JSON.parse(k.source_memory_ids);
        if (!Array.isArray(memIds)) continue;
        for (const mid of memIds) {
          const mem = db.prepare('SELECT id FROM memories WHERE id = ?').get(mid);
          if (!mem) {
            issues.push({ type: 'orphaned_knowledge', description: `Knowledge ${k.id} references missing memory ${mid}`, ids: [k.id, mid] });
            break;
          }
        }
      } catch {}
    }
  } catch {}

  // 2. Entity references pointing to deleted entities
  try {
    const badSubject = db.prepare(`
      SELECT id FROM knowledge WHERE subject_entity_id IS NOT NULL
      AND subject_entity_id NOT IN (SELECT id FROM entities)
    `).all();
    for (const k of badSubject) {
      issues.push({ type: 'orphaned_entity_ref', description: `Knowledge ${k.id} has dangling subject_entity_id`, ids: [k.id] });
    }
    const badObject = db.prepare(`
      SELECT id FROM knowledge WHERE object_entity_id IS NOT NULL
      AND object_entity_id NOT IN (SELECT id FROM entities)
    `).all();
    for (const k of badObject) {
      issues.push({ type: 'orphaned_entity_ref', description: `Knowledge ${k.id} has dangling object_entity_id`, ids: [k.id] });
    }
  } catch {}

  // 3. SQLite integrity check
  let integrityOk = true;
  try {
    const result = db.pragma('integrity_check');
    integrityOk = result === 'ok' || (Array.isArray(result) && result[0]?.integrity_check === 'ok');
    if (!integrityOk) {
      issues.push({ type: 'sqlite_integrity', description: 'SQLite integrity check failed', ids: [] });
    }
  } catch (err) {
    issues.push({ type: 'sqlite_integrity', description: `Integrity check error: ${err.message}`, ids: [] });
  }

  // 4. Source/session memory consistency
  try {
    const sourceScan = scanSourceIntegrity();
    issues.push(...sourceScan.issues);
  } catch {}

  // 5. Memory index and embedding consistency
  try {
    issues.push(...scanMemoryIndexIntegrity().issues);
  } catch {}
  try {
    issues.push(...scanEmbeddingIntegrity().issues);
  } catch {}

  const stats = {
    total_issues: issues.length,
    sqlite_ok: integrityOk,
    orphaned_knowledge: issues.filter(i => i.type === 'orphaned_knowledge').length,
    orphaned_entity_refs: issues.filter(i => i.type === 'orphaned_entity_ref').length,
    source_issues: issues.filter(i => i.type.startsWith('source_')).length,
    memory_index_issues: issues.filter(i => i.type.startsWith('memory_index_')).length,
    embedding_issues: issues.filter(i => i.type.startsWith('embedding_')).length,
  };

  return { issues, stats };
}

/**
 * Prune orphaned entity references (set to NULL).
 * @returns {object} { pruned: number }
 */
function pruneOrphanedRefs() {
  const db = brain.getDb();
  let pruned = 0;
  const fix = db.transaction(() => {
    const r1 = db.prepare(`
      UPDATE knowledge SET subject_entity_id = NULL
      WHERE subject_entity_id IS NOT NULL
      AND subject_entity_id NOT IN (SELECT id FROM entities)
    `).run();
    pruned += r1.changes;
    const r2 = db.prepare(`
      UPDATE knowledge SET object_entity_id = NULL
      WHERE object_entity_id IS NOT NULL
      AND object_entity_id NOT IN (SELECT id FROM entities)
    `).run();
    pruned += r2.changes;
  });
  fix();
  return { pruned };
}

/**
 * Rebuild memory_index from scratch.
 * @returns {object} { rebuilt: number }
 */
function rebuildMemoryIndex() {
  const db = brain.getDb();
  const { generateMemoryIndex } = require('../extraction/indexer');

  db.prepare('DELETE FROM memory_index').run();

  let rebuilt = 0;
  let offset = 0;
  const BATCH = 1000;

  while (true) {
    const batch = db.prepare(`
      SELECT id, content, domain, topic, timestamp FROM memories
      WHERE content IS NOT NULL AND length(content) > 20
      ORDER BY id ASC LIMIT ? OFFSET ?
    `).all(BATCH, offset);
    if (batch.length === 0) break;

    const insert = db.transaction(() => {
      for (const mem of batch) {
        const entry = generateMemoryIndex(mem);
        if (entry) {
          try { brain.insertMemoryIndex(entry); rebuilt++; } catch {}
        }
      }
    });
    insert();
    offset += batch.length;
  }

  // Reset backfill index cursor
  brain.setKv('backfill:index:cursor', null);

  return { rebuilt };
}

function scanSourceIntegrity({ sourceId = '', limit = 1000 } = {}) {
  const db = brain.getDb();
  const issues = [];
  const params = [];
  const conditions = [
    'archived_at IS NULL',
    "(source LIKE '%-jsonl' OR memory_type LIKE 'coding_session_%' OR source = 'walle-diary')",
  ];
  if (sourceId) {
    conditions.push('(source_id = ? OR source_id LIKE ? OR metadata LIKE ?)');
    params.push(sourceId, `${sourceId}:%`, `%"sourceId":"${sourceId}"%`);
  }
  params.push(Math.min(Math.max(Number(limit) || 1000, 1), 10000));

  const rows = db.prepare(`
    SELECT id, source, source_id, memory_type, metadata
    FROM memories
    WHERE ${conditions.join(' AND ')}
    ORDER BY timestamp DESC
    LIMIT ?
  `).all(...params);

  for (const row of rows) {
    const metadata = parseMetadata(row.metadata);
    const stableSourceId = metadata.sourceId || inferSourceId(row.source_id);
    if (!stableSourceId) {
      issues.push({
        type: 'source_missing_source_id',
        description: `Memory ${row.id} has no stable source id`,
        ids: [row.id],
      });
    }
    if (row.source !== 'walle-diary' && !metadata.sourceFile) {
      issues.push({
        type: 'source_missing_raw_ref',
        description: `Memory ${row.id} has no sourceFile metadata for replay`,
        ids: [row.id, stableSourceId || row.source_id],
      });
    }
    if (metadata.sourceFile && !fs.existsSync(metadata.sourceFile)) {
      issues.push({
        type: 'source_missing_file',
        description: `Memory ${row.id} references missing source file ${metadata.sourceFile}`,
        ids: [row.id, stableSourceId || row.source_id],
      });
    }
  }

  return {
    issues,
    stats: {
      scanned: rows.length,
      total_issues: issues.length,
      missing_source_id: issues.filter(i => i.type === 'source_missing_source_id').length,
      missing_raw_ref: issues.filter(i => i.type === 'source_missing_raw_ref').length,
      missing_file: issues.filter(i => i.type === 'source_missing_file').length,
    },
  };
}

function scanMemoryIndexIntegrity() {
  const db = brain.getDb();
  const issues = [];
  const hasIndex = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='memory_index'").get();
  if (!hasIndex) return { issues, stats: { scanned: 0, total_issues: 0 } };

  const orphaned = db.prepare(`
    SELECT mi.memory_id
    FROM memory_index mi
    LEFT JOIN memories m ON m.id = mi.memory_id
    WHERE m.id IS NULL
    LIMIT 1000
  `).all();
  for (const row of orphaned) {
    issues.push({
      type: 'memory_index_orphan',
      description: `memory_index references missing memory ${row.memory_id}`,
      ids: [row.memory_id],
    });
  }

  const missing = db.prepare(`
    SELECT m.id
    FROM memories m
    LEFT JOIN memory_index mi ON mi.memory_id = m.id
    WHERE m.archived_at IS NULL
      AND length(m.content) > 20
      AND mi.memory_id IS NULL
      AND (m.source LIKE '%-jsonl' OR m.memory_type LIKE 'coding_session_%' OR m.source = 'walle-diary')
    LIMIT 1000
  `).all();
  for (const row of missing) {
    issues.push({
      type: 'memory_index_missing',
      description: `Coding/session memory ${row.id} has no source index row`,
      ids: [row.id],
    });
  }

  return {
    issues,
    stats: {
      orphaned: orphaned.length,
      missing: missing.length,
      total_issues: issues.length,
    },
  };
}

function scanEmbeddingIntegrity() {
  const db = brain.getDb();
  const issues = [];
  const hasMap = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='embedding_map'").get();
  if (!hasMap) return { issues, stats: { checked: false, total_issues: 0 } };

  const orphanedMemories = db.prepare(`
    SELECT em.entity_id
    FROM embedding_map em
    LEFT JOIN memories m ON m.id = em.entity_id
    WHERE em.entity_type = 'memory' AND m.id IS NULL
    LIMIT 1000
  `).all();
  for (const row of orphanedMemories) {
    issues.push({ type: 'embedding_orphan_memory', description: `Embedding references missing memory ${row.entity_id}`, ids: [row.entity_id] });
  }

  const orphanedKnowledge = db.prepare(`
    SELECT em.entity_id
    FROM embedding_map em
    LEFT JOIN knowledge k ON k.id = em.entity_id
    WHERE em.entity_type = 'knowledge' AND k.id IS NULL
    LIMIT 1000
  `).all();
  for (const row of orphanedKnowledge) {
    issues.push({ type: 'embedding_orphan_knowledge', description: `Embedding references missing knowledge ${row.entity_id}`, ids: [row.entity_id] });
  }

  const models = db.prepare('SELECT model, COUNT(*) as c FROM embedding_map GROUP BY model').all();
  for (const model of models) {
    const vecTable = embeddingVecTableName(model.model);
    const table = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name=?").get(vecTable);
    if (!table) {
      issues.push({ type: 'embedding_missing_vec_table', description: `Embedding map has model ${model.model} but no ${vecTable}`, ids: [model.model] });
      continue;
    }
    try {
      const vecCount = db.prepare(`SELECT COUNT(*) as c FROM "${vecTable}"`).get().c;
      if (vecCount !== model.c) {
        issues.push({
          type: 'embedding_vec_count_mismatch',
          description: `Embedding map model ${model.model} has ${model.c} rows but ${vecTable} has ${vecCount}`,
          ids: [model.model],
        });
      }
    } catch {}
  }

  return {
    issues,
    stats: {
      models: models.length,
      orphaned_memory_embeddings: orphanedMemories.length,
      orphaned_knowledge_embeddings: orphanedKnowledge.length,
      total_issues: issues.length,
    },
  };
}

function embeddingVecTableName(model) {
  let key = String(model || '').replace(/[^A-Za-z0-9_]/g, '_');
  try {
    const { EMBEDDING_MODELS } = require('../embeddings');
    key = EMBEDDING_MODELS?.[model]?.key || key;
  } catch {}
  return `embedding_vec_${key}`;
}

async function replaySource({ adapterId, uri, sourceFile, sourceId, cwd = '', privacyClass, privacyFloor = null, dryRun = true, metadata = {} } = {}) {
  ensureBuiltinSourceAdapters();
  const adapter = sourceRegistry.instantiate(adapterId);
  if (!adapter) throw new Error(`Unknown source adapter: ${adapterId}`);
  const sourceRef = {
    adapterId,
    uri: uri || sourceFile,
    sourceFile: sourceFile || uri,
    sourceId,
    cwd,
    privacyClass,
    metadata,
  };
  if (dryRun) {
    const { records, diagnostics, source } = await collectIngestRecords(adapter, sourceRef);
    return {
      dryRun: true,
      adapterId,
      sourceId: source.sourceId,
      records: records.length,
      memoryRecords: records.filter((record) => record.type === RECORD_TYPES.MEMORY_RECORD).length,
      diagnostics,
    };
  }
  const { createSessionIngestService } = require('../memory/session-ingest-service');
  return createSessionIngestService({ brain }).ingestSource(sourceRef, { privacyFloor, dryRun: false });
}

function parseMetadata(value) {
  try {
    const parsed = JSON.parse(value || '{}');
    return parsed && typeof parsed === 'object' ? parsed : {};
  } catch {
    return {};
  }
}

function inferSourceId(sourceId = '') {
  const parts = String(sourceId || '').split(':');
  if (parts.length <= 2) return sourceId || '';
  return parts.slice(0, 2).join(':');
}

module.exports = {
  pruneOrphanedRefs,
  rebuildMemoryIndex,
  rebuildSourceIndex,
  replaySource,
  scanEmbeddingIntegrity,
  scanIntegrity,
  scanMemoryIndexIntegrity,
  scanSourceIntegrity,
};
