'use strict';

/**
 * Entity normalizer: rule-based entity detection and linking.
 * Finds or creates entities for knowledge triple subjects/objects.
 * Hybrid: rule-based first, Claude fallback only for ambiguous cases.
 * G6: i18n support — loads patterns from JSON files per language.
 */

const brain = require('../brain');
const path = require('path');
const fs = require('fs');

// G6: Load i18n entity patterns
const I18N_DIR = path.join(__dirname, 'i18n');
const _i18nPatterns = {};
function loadI18nPatterns() {
  if (Object.keys(_i18nPatterns).length > 0) return _i18nPatterns;
  try {
    const files = fs.readdirSync(I18N_DIR).filter(f => f.endsWith('.json'));
    for (const file of files) {
      const lang = file.replace('.json', '');
      _i18nPatterns[lang] = JSON.parse(fs.readFileSync(path.join(I18N_DIR, file), 'utf8'));
    }
  } catch {}
  return _i18nPatterns;
}

// Base English patterns (always available)
const ENTITY_TYPE_PATTERNS = {
  tool: /\b(vs ?code|vim|emacs|git|docker|kubernetes|k8s|terraform|jenkins|github|gitlab|jira|confluence|slack|notion|figma|linear|vercel|aws|gcp|azure|heroku|fly\.io|cloudflare|postgres|mysql|redis|sqlite|mongodb|react|vue|angular|svelte|nextjs|express|django|flask|spring|node|python|java|rust|go|typescript|javascript)\b/i,
  project: /\b(wall-?e|ctm|claude.task.manager|mempalace)\b/i,
  org: /\b(google|apple|microsoft|amazon|meta|facebook|anthropic|openai|netflix|uber|airbnb|stripe|github)\b/i,
  concept: /\b(tdd|ci\/cd|devops|agile|scrum|kanban|microservices?|monolith|serverless|rest|graphql|oauth|jwt|websocket)\b/i,
};

/**
 * Detect entity type from text. Returns type string or null.
 * G6: Checks i18n patterns (Chinese, etc.) in addition to English.
 */
function detectEntityType(text) {
  if (!text) return null;
  // English patterns first
  for (const [type, pattern] of Object.entries(ENTITY_TYPE_PATTERNS)) {
    if (pattern.test(text)) return type;
  }
  // Capitalized proper noun heuristic (2+ words, each capitalized)
  if (/^[A-Z][a-z]+(\s+[A-Z][a-z]+)+$/.test(text.trim())) return 'person';
  // Single capitalized word (3+ chars)
  if (/^[A-Z][a-z]{2,}$/.test(text.trim())) return 'person';

  // G6: Check i18n patterns
  const i18n = loadI18nPatterns();
  for (const [_lang, patterns] of Object.entries(i18n)) {
    if (patterns.entity_types) {
      for (const [type, regex] of Object.entries(patterns.entity_types)) {
        try {
          if (new RegExp(regex, 'i').test(text)) return type;
        } catch {}
      }
    }
    // Chinese person name pattern (2-4 CJK chars)
    if (patterns.person_name_pattern) {
      try {
        if (new RegExp(`^${patterns.person_name_pattern}$`).test(text.trim())) return 'person';
      } catch {}
    }
  }
  return null;
}

/**
 * Normalize an entity name: lowercase, trim, remove common prefixes.
 */
function normalizeName(text) {
  if (!text) return '';
  return text.trim()
    .replace(/^(mr|mrs|ms|dr|prof)\.?\s+/i, '')
    .replace(/\s+/g, ' ');
}

/**
 * MemPalace-style 2-pass entity scoring.
 * Scans surrounding text for signal patterns to classify entity type.
 * @param {string} name - Entity name to score
 * @param {string} surroundingText - Text around the entity (±200 chars)
 * @returns {object|null} { person_score, project_score, tool_score, signal_categories, entity_type, confidence }
 */
function scoreEntitySignals(name, surroundingText) {
  if (!name || !surroundingText) return null;

  const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
  const text = surroundingText;

  let person_score = 0;
  let project_score = 0;
  let tool_score = 0;
  const categoriesHit = new Set();

  // --- Pass 1: Person signals ---

  // Dialogue markers (x3)
  const dialoguePatterns = [
    new RegExp(`${escaped}\\s+said\\b`, 'i'),
    new RegExp(`${escaped}\\s+asked\\b`, 'i'),
    new RegExp(`Speaker:\\s*${escaped}`, 'i'),
    new RegExp(`${escaped}:\\s*"`, 'i'),
  ];
  for (const p of dialoguePatterns) {
    if (p.test(text)) {
      person_score += 3;
      categoriesHit.add('dialogue');
    }
  }

  // Person verbs (x2)
  const personVerbs = ['smiled', 'laughed', 'decided', 'felt', 'wanted', 'thought'];
  for (const verb of personVerbs) {
    const re = new RegExp(`${escaped}\\s+${verb}\\b`, 'i');
    if (re.test(text)) {
      person_score += 2;
      categoriesHit.add('person_verb');
    }
  }

  // Pronoun proximity (x2): personal pronouns within 100 chars after name
  const nameIdx = text.toLowerCase().indexOf(name.toLowerCase());
  if (nameIdx >= 0) {
    const window = text.slice(nameIdx, nameIdx + name.length + 100);
    const pronouns = /\b(he|she|they|him|her|his|hers|their)\b/i;
    if (pronouns.test(window)) {
      person_score += 2;
      categoriesHit.add('pronoun');
    }
  }

  // Direct address (x4)
  const directPatterns = [
    new RegExp(`\\bhi\\s+${escaped}\\b`, 'i'),
    new RegExp(`\\bhey\\s+${escaped}\\b`, 'i'),
    new RegExp(`\\bthanks\\s+${escaped}\\b`, 'i'),
    new RegExp(`@${escaped}\\b`, 'i'),
  ];
  for (const p of directPatterns) {
    if (p.test(text)) {
      person_score += 4;
      categoriesHit.add('direct_address');
    }
  }

  // --- Pass 2: Project/tool signals ---

  // Project verbs (x2)
  const projectVerbs = ['building', 'shipped', 'deployed', 'released', 'launched'];
  for (const verb of projectVerbs) {
    const re = new RegExp(`${verb}\\s+${escaped}\\b`, 'i');
    if (re.test(text)) {
      project_score += 2;
      categoriesHit.add('project_verb');
    }
  }

  // Versioned patterns (x3)
  const versionPatterns = [
    new RegExp(`${escaped}\\s+v\\d`, 'i'),
    new RegExp(`${escaped}-\\w+`, 'i'),
    new RegExp(`${escaped}\\.\\w+`, 'i'),
  ];
  for (const p of versionPatterns) {
    if (p.test(text)) {
      project_score += 3;
      categoriesHit.add('versioned');
    }
  }

  // Code refs (x3)
  const codePatterns = [
    new RegExp(`${escaped}\\.js\\b`, 'i'),
    new RegExp(`${escaped}\\.py\\b`, 'i'),
    new RegExp(`\\bimport\\s+${escaped}\\b`, 'i'),
    new RegExp(`require\\(['"]${escaped}['"]\\)`, 'i'),
    new RegExp(`\\bfrom\\s+${escaped}\\b`, 'i'),
  ];
  for (const p of codePatterns) {
    if (p.test(text)) {
      tool_score += 3;
      categoriesHit.add('code_ref');
    }
  }

  // --- Classification ---
  const total = person_score + project_score + tool_score;
  if (total === 0) return null;

  const signal_categories = categoriesHit.size;
  const person_ratio = person_score / (person_score + project_score + tool_score + 0.001);

  let entity_type = null;
  let confidence = 0;

  if (person_ratio >= 0.7 && signal_categories >= 2 && person_score >= 5) {
    entity_type = 'person';
    confidence = Math.min(0.99, 0.5 + person_ratio * 0.5);
  } else if (tool_score > person_score && tool_score > project_score) {
    entity_type = 'tool';
    confidence = 0.7;
  } else if (person_ratio < 0.3) {
    entity_type = 'project';
    confidence = 0.7;
  }

  if (!entity_type) return null;

  return { person_score, project_score, tool_score, signal_categories, entity_type, confidence };
}

// Words that should never be entities (expanded from MemPalace)
const COMMON_WORDS = new Set([
  'it', 'they', 'we', 'he', 'she', 'this', 'that', 'owner', 'user', 'someone', 'anyone',
  'the', 'and', 'for', 'are', 'but', 'not', 'you', 'all', 'can', 'had', 'her', 'was',
  'one', 'our', 'out', 'has', 'his', 'how', 'its', 'let', 'say', 'too', 'use',
  'what', 'when', 'where', 'which', 'with', 'would', 'could', 'should', 'about',
  'after', 'before', 'between', 'does', 'each', 'from', 'have', 'just', 'like',
  'make', 'many', 'more', 'most', 'much', 'must', 'only', 'over', 'such',
  'take', 'than', 'them', 'then', 'very', 'well', 'were', 'will', 'your',
  'also', 'been', 'both', 'come', 'made', 'find', 'here', 'know', 'look',
  'some', 'thing', 'think', 'want', 'give', 'good', 'tell', 'time', 'work',
  'call', 'need', 'back', 'long', 'keep', 'last', 'still', 'might', 'going',
  'todo', 'done', 'true', 'false', 'null', 'undefined', 'error', 'warning',
  'note', 'info', 'debug', 'test', 'main', 'file', 'data', 'type', 'code',
]);

/**
 * Find or create an entity for a given text string.
 * @param {string} text - The entity text (e.g., "John Smith", "PostgreSQL")
 * @param {string} [ownerName] - Owner name for special handling
 * @param {string} [surroundingText] - Context around the entity for multi-pass scoring
 * @returns {object|null} Entity object or null if text is too generic
 */
function findOrCreateEntity(text, ownerName, surroundingText) {
  if (!text || text.length < 2) return null;

  const normalized = normalizeName(text);
  if (!normalized || normalized.length < 2) return null;

  // Skip very generic terms
  if (COMMON_WORDS.has(normalized.toLowerCase())) return null;

  // Check if this is the owner
  if (ownerName && normalized.toLowerCase() === ownerName.toLowerCase()) {
    const existing = brain.findEntity(ownerName);
    if (existing) return existing;
    const result = brain.insertEntity({
      canonical_name: ownerName,
      entity_type: 'person',
      aliases: JSON.stringify([]),
      properties: JSON.stringify({ is_owner: true, confidence: 1.0, confidence_source: 'onboarding' }),
    });
    return brain.getEntity(result.id);
  }

  // Try to find existing entity
  const existing = brain.findEntityFuzzy(normalized);
  if (existing) return existing;

  // Check people table FIRST (cheap lookup, yields onboarding confidence)
  try {
    const people = brain.listPeople({});
    const match = people.find(p =>
      p.name && p.name.toLowerCase() === normalized.toLowerCase()
    );
    if (match) {
      const result = brain.insertEntity({
        canonical_name: match.name,
        entity_type: 'person',
        aliases: match.aliases || '[]',
        properties: JSON.stringify({
          person_id: match.id,
          confidence: 1.0,
          confidence_source: 'onboarding',
        }),
      });
      return brain.getEntity(result.id);
    }
  } catch {}

  // Detect type
  let entityType = detectEntityType(normalized);
  let signalResult = null;

  // GAP A: If rule-based detection fails, try multi-pass scoring with surrounding text
  if (!entityType && surroundingText) {
    signalResult = scoreEntitySignals(normalized, surroundingText);
    if (signalResult) {
      entityType = signalResult.entity_type;
    }
  }

  if (!entityType) return null; // Can't determine type, skip

  // Build properties with confidence tier
  const props = {};
  if (signalResult) {
    props.confidence = signalResult.confidence;
    props.confidence_source = 'detected';
    props.signal_categories = signalResult.signal_categories;
    props.person_score = signalResult.person_score;
    props.project_score = signalResult.project_score;
    props.tool_score = signalResult.tool_score;
  } else {
    // Rule-based detection — moderate confidence
    props.confidence = 0.7;
    props.confidence_source = 'detected';
  }

  // Create new entity
  const result = brain.insertEntity({
    canonical_name: normalized,
    entity_type: entityType,
    aliases: JSON.stringify([]),
    properties: JSON.stringify(props),
  });
  return brain.getEntity(result.id);
}

/**
 * Link a knowledge entry to entities (subject and object).
 * @param {object} entry - Knowledge entry with subject, object, id
 * @param {string} [ownerName] - Owner name
 * @returns {object} { subjectEntity, objectEntity }
 */
function linkKnowledgeToEntities(entry, ownerName) {
  if (!entry || !entry.id) return { subjectEntity: null, objectEntity: null };

  const subjectEntity = findOrCreateEntity(entry.subject, ownerName);
  const objectEntity = findOrCreateEntity(entry.object, ownerName);

  if (subjectEntity || objectEntity) {
    brain.updateKnowledgeEntityLinks(
      entry.id,
      subjectEntity ? subjectEntity.id : null,
      objectEntity ? objectEntity.id : null
    );
  }

  return { subjectEntity, objectEntity };
}

/**
 * Learn entities from text content. Scans for capitalized proper nouns,
 * scores by frequency and context, and creates/updates entities.
 * @param {string} text - Content to scan
 * @param {string} [ownerName] - Owner name
 * @returns {object} { created: number, updated: number }
 */
function learnEntitiesFromText(text, ownerName) {
  if (!text || text.length < 50) return { created: 0, updated: 0 };

  let created = 0, updated = 0;

  // Extract capitalized multi-word names (Person Name pattern)
  const namePattern = /\b([A-Z][a-z]+(?:\s+[A-Z][a-z]+)+)\b/g;
  const nameCounts = {};
  let match;
  while ((match = namePattern.exec(text)) !== null) {
    const name = match[1];
    if (COMMON_WORDS.has(name.toLowerCase())) continue;
    nameCounts[name] = (nameCounts[name] || 0) + 1;
  }

  // G6: Extract CJK names (2-4 chars preceded by dialogue verbs or titles)
  const i18n = loadI18nPatterns();
  const zhPatterns = i18n.zh;
  if (zhPatterns) {
    const zhTitles = (zhPatterns.person_signals?.titles || []).join('|');
    const zhVerbs = (zhPatterns.person_signals?.dialogue_verbs || []).join('|');
    const zhNameRe = new RegExp('(?:' + zhTitles + '|' + zhVerbs + ')[\\s]*([\u4e00-\u9fff]{2,4})', 'g');
    while ((match = zhNameRe.exec(text)) !== null) {
      const name = match[1];
      nameCounts[name] = (nameCounts[name] || 0) + 1;
    }
  }

  // Only create entities for names appearing 2+ times
  for (const [name, count] of Object.entries(nameCounts)) {
    if (count < 2) continue;

    // Extract surrounding text (±200 chars) around first occurrence for scoring
    const nameIdx = text.indexOf(name);
    const surroundingText = nameIdx >= 0
      ? text.slice(Math.max(0, nameIdx - 200), nameIdx + name.length + 200)
      : '';

    const existing = brain.findEntityFuzzy(name);
    if (existing) {
      // Boost confidence for seen-again entities, but don't downgrade onboarding
      try {
        const props = JSON.parse(existing.properties || '{}');
        props.seen_count = (props.seen_count || 0) + count;
        // Don't downgrade onboarding confidence
        if (props.confidence_source !== 'onboarding') {
          const learnedConf = Math.min(0.75 + count * 0.05, 0.95);
          if (!props.confidence || learnedConf > props.confidence) {
            props.confidence = learnedConf;
            props.confidence_source = 'learned';
          }
        }
        brain.getDb().prepare(
          "UPDATE entities SET properties = ?, updated_at = datetime('now') WHERE id = ?"
        ).run(JSON.stringify(props), existing.id);
        updated++;
      } catch {}
    } else {
      // Try multi-pass scoring for better type detection
      let entityType = detectEntityType(name);
      let signalResult = null;
      if (!entityType && surroundingText) {
        signalResult = scoreEntitySignals(name, surroundingText);
        if (signalResult) entityType = signalResult.entity_type;
      }
      entityType = entityType || 'person';

      // Learned confidence: 0.75 + count * 0.05, max 0.95
      const confidence = Math.min(0.75 + count * 0.05, 0.95);
      const props = {
        seen_count: count,
        confidence,
        confidence_source: 'learned',
      };
      if (signalResult) {
        props.signal_categories = signalResult.signal_categories;
        props.person_score = signalResult.person_score;
        props.project_score = signalResult.project_score;
        props.tool_score = signalResult.tool_score;
      }

      try {
        brain.insertEntity({
          canonical_name: name,
          entity_type: entityType,
          aliases: JSON.stringify([]),
          properties: JSON.stringify(props),
        });
        created++;
      } catch {}
    }
  }

  return { created, updated };
}

/**
 * Update entity confidence based on access patterns.
 * Entities referenced more often get higher confidence.
 */
function updateEntityConfidence() {
  const db = brain.getDb();
  const entities = brain.listEntities({ limit: 1000 });
  let updated = 0;

  for (const entity of entities) {
    // Count knowledge triples referencing this entity
    const refCount = db.prepare(`
      SELECT COUNT(*) as c FROM knowledge
      WHERE (subject_entity_id = ? OR object_entity_id = ?) AND status = 'active'
    `).get(entity.id, entity.id).c;

    if (refCount > 0) {
      const newConfidence = Math.min(0.3 + refCount * 0.05, 1.0);
      try {
        const props = JSON.parse(entity.properties || '{}');
        // Never downgrade onboarding confidence (GAP H: 3-tier)
        if (props.confidence_source === 'onboarding') continue;
        if (props.confidence !== newConfidence) {
          props.confidence = newConfidence;
          props.ref_count = refCount;
          db.prepare("UPDATE entities SET properties = ?, updated_at = datetime('now') WHERE id = ?")
            .run(JSON.stringify(props), entity.id);
          updated++;
        }
      } catch {}
    }
  }

  return { updated };
}

module.exports = { findOrCreateEntity, linkKnowledgeToEntities, detectEntityType, normalizeName, learnEntitiesFromText, updateEntityConfidence, scoreEntitySignals, COMMON_WORDS };
