// Context Agent - Extracts and manages game state from narration

const axios = require('axios');
const LM_STUDIO_URL = 'http://localhost:1234';

const CONTEXT_EXTRACTION_PROMPT = `You are the CONTEXT AGENT for a text-based RPG.
Your job is to extract structured game state from narration text.

Analyze the narration and extract:

1. CHARACTERS (NPCs mentioned, not the player):
   - Name (exact as written)
   - Description (physical appearance, role)
   - Traits (personality, characteristics)
   - Gender hint (if mentioned: "he", "she", "man", "woman", etc.)
   - Status (Active, Hostile, Friendly, Unknown)

2. LOCATIONS (new places or significant location changes):
   - Name
   - Description
   - Atmosphere (mood, feeling)
   - Points of interest (notable objects, features)

3. ITEMS (items gained, lost, or used):
   - Name
   - Description
   - Type (weapon, armor, consumable, quest, misc)
   - Action (acquired, lost, used, examined)

4. QUESTS (quests mentioned, started, completed, updated):
   - Title
   - Description
   - Status (Active, Completed, Failed, Updated)
   - Objectives (list of goals)

5. STORY STATE (important facts, changes to world):
   - Key-value pairs of important information

6. JOURNAL UPDATES:
   - summaryUpdate: 1-2 sentence summary of what happened
   - keyEvent: Significant event description (if any)
   - characterArcNotes: Character development notes {characterName: notes}

Return ONLY valid JSON in this exact format:
{
  "characters": [{"name": "", "description": "", "traits": [], "gender": "", "status": ""}],
  "locations": [{"name": "", "description": "", "atmosphere": "", "pointsOfInterest": []}],
  "items": [{"name": "", "description": "", "type": "", "action": ""}],
  "quests": [{"title": "", "description": "", "status": "", "objectives": []}],
  "storyState": {"key": "value"},
  "journal": {
    "summaryUpdate": "",
    "keyEvent": "",
    "characterArcNotes": {}
  }
}

If nothing to extract for a category, return empty array or null.
Be thorough but only extract NEW or CHANGED information.`;

class ContextAgent {
  constructor() {
    this.model = 'local-model';
    this.maxRetries = 2;
  }

  async extract(narration, existingContext = {}) {
    console.log('[ContextAgent] Starting extraction...');
    
    try {
      const response = await axios.post(`${LM_STUDIO_URL}/v1/chat/completions`, {
        model: this.model,
        messages: [
          { 
            role: 'system', 
            content: CONTEXT_EXTRACTION_PROMPT 
          },
          { 
            role: 'user', 
            content: `Existing context:
Characters: ${JSON.stringify(existingContext.characters || []).substring(0, 500)}
Locations: ${JSON.stringify(existingContext.locations || []).substring(0, 500)}

Narration to analyze:
${narration}

Extract context updates:` 
          }
        ],
        temperature: 0.3,
        max_tokens: 2000,
        stream: false
      }, {
        timeout: 60000
      });

      const content = response.data.choices[0].message.content;
      
      // Extract JSON from response
      const jsonMatch = content.match(/\{[\s\S]*\}/);
      if (!jsonMatch) {
        console.log('[ContextAgent] No JSON found, returning empty');
        return this.createEmptyExtraction();
      }

      const extracted = JSON.parse(jsonMatch[0]);
      console.log('[ContextAgent] Extraction complete');
      
      return {
        success: true,
        data: extracted
      };

    } catch (error) {
      console.error('[ContextAgent] Error:', error.message);
      return {
        success: false,
        error: error.message,
        data: this.createEmptyExtraction()
      };
    }
  }

  createEmptyExtraction() {
    return {
      characters: [],
      locations: [],
      items: [],
      quests: [],
      storyState: {},
      journal: {
        summaryUpdate: null,
        keyEvent: null,
        characterArcNotes: {}
      }
    };
  }

  // Merge extracted data with existing context
  mergeContext(existing, extracted) {
    const merged = {
      characters: this.mergeCharacters(existing.characters || [], extracted.characters || []),
      locations: this.mergeLocations(existing.locations || [], extracted.locations || []),
      inventory: existing.inventory || [],
      quests: this.mergeQuests(existing.quests || [], extracted.quests || []),
      storyState: { ...existing.storyState, ...extracted.storyState }
    };

    // Handle inventory updates
    if (extracted.items && extracted.items.length > 0) {
      merged.inventory = this.updateInventory(existing.inventory || [], extracted.items);
    }

    return merged;
  }

  mergeCharacters(existing, newChars) {
    const normalized = existing.map(c => ({
      ...c,
      normalizedName: this.normalizeName(c.name)
    }));

    for (const newChar of newChars) {
      if (!newChar.name) continue;

      const normalizedName = this.normalizeName(newChar.name);
      const existingIndex = normalized.findIndex(c => c.normalizedName === normalizedName);

      if (existingIndex >= 0) {
        // Merge with existing
        normalized[existingIndex] = {
          ...normalized[existingIndex],
          ...newChar,
          traits: [...new Set([...(normalized[existingIndex].traits || []), ...(newChar.traits || [])])]
        };
      } else {
        // Add new character
        normalized.push({
          ...newChar,
          normalizedName,
          traits: newChar.traits || [],
          status: newChar.status || 'Active'
        });
      }
    }

    return normalized.map(({ normalizedName, ...rest }) => rest);
  }

  mergeLocations(existing, newLocations) {
    const normalized = existing.map(l => ({
      ...l,
      normalizedName: this.normalizeName(l.name)
    }));

    for (const newLoc of newLocations) {
      if (!newLoc.name) continue;

      const normalizedName = this.normalizeName(newLoc.name);
      const existingIndex = normalized.findIndex(l => l.normalizedName === normalizedName);

      if (existingIndex >= 0) {
        normalized[existingIndex] = {
          ...normalized[existingIndex],
          ...newLoc,
          pointsOfInterest: [...new Set([...(normalized[existingIndex].pointsOfInterest || []), ...(newLoc.pointsOfInterest || [])])]
        };
      } else {
        normalized.push({
          ...newLoc,
          normalizedName,
          atmosphere: newLoc.atmosphere || 'Neutral',
          pointsOfInterest: newLoc.pointsOfInterest || []
        });
      }
    }

    return normalized.map(({ normalizedName, ...rest }) => rest);
  }

  mergeQuests(existing, newQuests) {
    const merged = [...existing];

    for (const newQuest of newQuests) {
      if (!newQuest.title) continue;

      const existingIndex = merged.findIndex(q => q.title === newQuest.title);

      if (existingIndex >= 0) {
        merged[existingIndex] = {
          ...merged[existingIndex],
          ...newQuest,
          objectives: newQuest.objectives || merged[existingIndex].objectives
        };
      } else {
        merged.push({
          ...newQuest,
          status: newQuest.status || 'Active',
          objectives: newQuest.objectives || []
        });
      }
    }

    return merged;
  }

  updateInventory(inventory, items) {
    let updated = [...inventory];

    for (const item of items) {
      if (!item.name) continue;

      const normalizedName = this.normalizeName(item.name);

      if (item.action === 'acquired') {
        const exists = updated.some(i => this.normalizeName(i.name) === normalizedName);
        if (!exists) {
          updated.push({
            name: item.name,
            description: item.description || '',
            type: item.type || 'misc',
            effects: item.effects || ''
          });
        }
      } else if (item.action === 'lost' || item.action === 'used') {
        updated = updated.filter(i => this.normalizeName(i.name) !== normalizedName);
      }
    }

    return updated;
  }

  normalizeName(name) {
    return name.toLowerCase().replace(/\s+/g, '_').replace(/[^a-z0-9_]/g, '');
  }
}

module.exports = ContextAgent;
