const brain = require('../brain');
const { extractKnowledge } = require('../extraction/knowledge-extractor');
const { detectContradictions } = require('../extraction/contradiction');
const { linkKnowledgeToEntities, learnEntitiesFromText } = require('../extraction/entity-normalizer');
const { shouldRunLoop, skipped } = require('./loop-precheck');
let embeddings;
try { embeddings = require('../embeddings'); } catch { embeddings = null; }

const BATCH_SIZE = 20;

// Contradiction → question policy. Historically EVERY detected contradiction created a
// pending_question AND superseded the old knowledge — yielding ~2,000 questions/day that
// piled up (125k unanswered) because contradictions are mostly routine knowledge churn
// (working dir / current task / tool changed). We now ALWAYS supersede (newer wins) and
// only ask the owner when the contradiction is genuinely ambiguous. The offline detector
// flags mechanical churn (value_conflict / relationship_mismatch / stale_fact = "X was A,
// now B"); the LLM path is only invoked for the ambiguous remainder. So: ask only on
// LLM-detected contradictions (no offline `type`) or when the new fact's confidence is low.
const _OFFLINE_CONTRADICTION_TYPES = new Set(['value_conflict', 'relationship_mismatch', 'stale_fact']);
const _ASK_CONTRADICTION_CONFIDENCE = Number(process.env.WALL_E_ASK_CONTRADICTION_CONFIDENCE || 0.4);
function _contradictionQuestionsEnabled() {
  return String(process.env.WALL_E_CONTRADICTION_QUESTIONS || 'on').toLowerCase() !== 'off';
}
function _shouldAskAboutContradiction(c) {
  if (!c) return false;
  if (c.type && _OFFLINE_CONTRADICTION_TYPES.has(c.type)) {
    // Mechanical churn → supersede silently, unless the NEW fact itself is low-confidence.
    const conf = Number(c.new_entry && c.new_entry.confidence);
    return Number.isFinite(conf) && conf < _ASK_CONTRADICTION_CONFIDENCE;
  }
  return true; // LLM-detected (ambiguous) → ask
}

async function runOnce(opts = {}) {
  // Centralized precheck: provider availability. (We don't pass
  // hasFreshSignal here because think.js still does work — self-resolve
  // questions — even with no new memories.)
  const pre = shouldRunLoop({ loopName: 'think' });
  if (!pre.run) {
    if (pre.reason === 'no-provider' && !runOnce._pauseLogged) {
      console.log('[think] No AI provider available -- pausing');
      runOnce._pauseLogged = true;
    }
    return skipped({ loopName: 'think', reason: pre.reason });
  }
  if (runOnce._pauseLogged) {
    console.log('[think] Provider available -- resuming');
    runOnce._pauseLogged = false;
  }

  const extractFn = opts.extractFn || extractKnowledge;
  const ownerName = brain.getOwnerName();

  const pending = brain.listMemories({ extractionStatus: 'pending', limit: BATCH_SIZE });
  if (pending.length === 0) {
    // Still run self-resolution even with no new memories
    await selfResolveQuestions(ownerName, opts);
    return { memoriesProcessed: 0, knowledgeExtracted: 0 };
  }

  let knowledgeExtracted = 0;
  let insertedIds = [];

  try {
    // Step 1: Extract knowledge + classifications (async API call)
    const entries = await extractFn(pending, ownerName, { model: opts.model });
    const classifications = entries._classifications || [];

    // Step 2: Detect contradictions (async API call, outside transaction)
    const contradictions = await detectContradictions(entries, ownerName, { detectFn: opts.detectFn });

    // Step 3: DB transaction: insert knowledge + handle contradictions + classify + mark done
    const db = brain.getDb();
    const writeResults = db.transaction(() => {
      // Insert all new knowledge entries, tracking their IDs
      for (const entry of entries) {
        const result = brain.insertKnowledge({
          category: entry.category,
          subject: entry.subject,
          predicate: entry.predicate,
          object: entry.object,
          confidence: entry.confidence || 0.5,
          source_memory_ids: JSON.stringify(entry.source_memory_ids || []),
        });
        insertedIds.push({ ...entry, _id: result.id });
        knowledgeExtracted++;
        // Link to entities (rule-based, no API cost)
        try { linkKnowledgeToEntities({ ...entry, id: result.id }, ownerName); } catch {}
      }

      // Handle contradictions
      for (const contradiction of contradictions) {
        // Find the ID of the newly inserted entry that matches this contradiction
        const newInserted = insertedIds.find(e =>
          e.subject === contradiction.new_entry.subject &&
          e.predicate === contradiction.new_entry.predicate &&
          e.object === contradiction.new_entry.object
        );

        // Always supersede old knowledge (newer wins) — keeps the graph current.
        if (newInserted) {
          brain.supersedeKnowledge(contradiction.old_id, newInserted._id);
        }

        // Only surface a question when genuinely worth the owner's input. Routine churn
        // is superseded silently above (see _shouldAskAboutContradiction) — this stops the
        // ~2,000/day pile-up while still escalating ambiguous/low-confidence conflicts.
        if (_contradictionQuestionsEnabled() && _shouldAskAboutContradiction(contradiction)) {
          brain.insertQuestion({
            question_type: 'contradiction',
            question: contradiction.explanation,
            context: JSON.stringify({
              old_id: contradiction.old_id,
              new_entry: contradiction.new_entry,
            }),
          });
        }
      }

      // Apply domain/topic classifications to memories
      for (const c of classifications) {
        try {
          brain.updateMemoryClassification(c.memory_id, c.domain, c.topic);
        } catch {}
      }

      for (const mem of pending) {
        brain.updateMemoryExtraction(mem.id, 'done');
      }
    });
    writeResults();
  } catch (err) {
    console.error('[think] Extraction failed:', err.message);
    for (const mem of pending) {
      try {
        brain.updateMemoryExtraction(mem.id, 'failed');
      } catch (updateErr) {
        console.error('[think] Failed to mark memory as failed:', updateErr.message);
      }
    }
  }

  // Step 4a: Learn entities from memory content (rule-based, no API cost)
  try {
    for (const mem of pending) {
      if (mem.content && mem.content.length > 50) {
        learnEntitiesFromText(mem.content, ownerName);
      }
    }
  } catch {}

  // Step 4: Compute embeddings for processed memories + new knowledge (async, non-blocking)
  if (embeddings && embeddings.isAvailable()) {
    try {
      const textsToEmbed = pending.map(m => (m.content || '').substring(0, 512));
      const memEmbeddings = await embeddings.batchComputeEmbeddings(textsToEmbed);
      if (memEmbeddings) {
        for (let i = 0; i < pending.length; i++) {
          embeddings.storeEmbedding(pending[i].id, 'memory', memEmbeddings[i]);
        }
        console.log(`[think] Stored ${pending.length} memory embeddings`);
      }

      // Embed new knowledge triples
      if (insertedIds.length > 0) {
        const kTexts = insertedIds.map(k => `${k.subject} ${k.predicate} ${k.object}`);
        const kEmbeddings = await embeddings.batchComputeEmbeddings(kTexts);
        if (kEmbeddings) {
          for (let i = 0; i < insertedIds.length; i++) {
            embeddings.storeEmbedding(insertedIds[i]._id, 'knowledge', kEmbeddings[i]);
          }
          console.log(`[think] Stored ${insertedIds.length} knowledge embeddings`);
        }
      }
    } catch (err) {
      console.error('[think] Embedding computation failed (non-fatal):', err.message);
    }
  }

  brain.upsertCheckpoint('think', {
    last_memory_at: pending[pending.length - 1].timestamp,
    metadata: JSON.stringify({ memories_processed: pending.length, knowledge_extracted: knowledgeExtracted }),
  });

  // Task 5: Self-resolve pending questions
  await selfResolveQuestions(ownerName, opts);

  return { memoriesProcessed: pending.length, knowledgeExtracted };
}

async function selfResolveQuestions(ownerName, opts = {}) {
  const pendingQuestions = brain.listQuestions({ status: 'pending', limit: 10 });

  for (const question of pendingQuestions) {
    // Get memories created after the question was created
    const newMemories = brain.listMemories({ since: question.created_at });

    // Need more than 3 new relevant memories to attempt resolution
    if (newMemories.length <= 3) continue;

    const selfResolveFn = opts.selfResolveFn;
    if (!selfResolveFn) continue;

    try {
      const result = await selfResolveFn(question, newMemories, ownerName);
      if (result && result.resolved) {
        brain.answerQuestion(question.id, {
          answer: result.answer,
          resolution_type: 'inferred',
          resolution_evidence: JSON.stringify(result.evidence),
        });
      }
    } catch (err) {
      console.error('[think] Self-resolution failed for question', question.id, ':', err.message);
    }
  }
}

module.exports = { runOnce, _shouldAskAboutContradiction };
