/**
 * Fractal Narrative Memory System
 * Implements multi-scale narrative memory with fractal segmentation,
 * graph-based relationship tracking, and motif clustering
 */

import { EventEmitter } from 'events';
import Database from 'better-sqlite3';
import nlp from 'compromise';
import { HolographicMemorySystem } from './hhm/holographic-memory-system.js';
import { getLogger } from '../../core/logger.js';
// import { AppError, ErrorCode } from '../../utils/common.js';
import type { ScrivenerDocument } from '../../types/index.js';

// ============================================================================
// Core Types and Interfaces
// ============================================================================

export interface MicroSegment {
	id: string;
	chapter: number;
	paraIndex: number;
	sentIndex: number;
	beatIndex?: number;
	text: string;
	startChar: number;
	endChar: number;
	embeddingId?: string;
	tokens: number;
}

export interface MesoSegment {
	id: string;
	chapter: number;
	startChar: number;
	endChar: number;
	text: string;
	microIds: string[];
	embeddingId?: string;
	sceneType?: 'action' | 'dialogue' | 'description' | 'transition';
	tokens: number;
}

export interface MacroSegment {
	id: string;
	chapterOrArc: string;
	startChar: number;
	endChar: number;
	text: string;
	mesoIds: string[];
	embeddingId?: string;
	arcType?: 'setup' | 'rising' | 'climax' | 'falling' | 'resolution';
}

export interface GraphNode {
	nodeId: string;
	nodeType: 'character' | 'object' | 'motif' | 'setting' | 'event' | 'segment';
	canonicalName: string;
	attributesJson: Record<string, any>;
	frequency: number;
	centrality?: number;
}

export interface GraphEdge {
	edgeId: string;
	fromNode: string;
	toNode: string;
	edgeType: 'interacts' | 'cooccurrence' | 'emotional' | 'causal' | 'temporal';
	weight: number;
	evidenceJson: Record<string, any>;
}

export interface MotifCluster {
	clusterId: number;
	keywords: string[];
	label?: string;
	segments: string[];
	centroid?: Float32Array;
	coherenceScore: number;
}

export interface FractalRetrievalResult {
	scale: 'micro' | 'meso' | 'macro';
	segmentId: string;
	score: number;
	segment: MicroSegment | MesoSegment | MacroSegment;
	graphBoost: number;
	contextBoost: number;
}

export interface FractalMemoryConfig {
	microTokenRange: [number, number]; // [5, 40]
	mesoTokenRange: [number, number]; // [100, 1200]
	mesoOverlap: number; // 50 tokens
	scaleWeights: {
		micro: number;
		meso: number;
		macro: number;
	};
	graphBoostWeight: number;
	contextBoostWeight: number;
	minClusterSize: number;
	embeddingModel: string;
}

// ============================================================================
// Fractal Segmentation Engine
// ============================================================================

export class FractalSegmenter {
	private logger: ReturnType<typeof getLogger>;
	// private sentenceTokenizer: unknown; // spaCy or similar
	private config: FractalMemoryConfig;

	constructor(config: FractalMemoryConfig) {
		this.logger = getLogger('FractalSegmenter');
		this.config = config;
	}

	async segment(
		chapterText: string,
		chapterIndex: number
	): Promise<{
		micro: MicroSegment[];
		meso: MesoSegment[];
		macro: MacroSegment[];
	}> {
		this.logger.debug(`Segmenting chapter ${chapterIndex}`);

		// 1. Create micro segments (sentences/beats)
		const microSegments = await this.createMicroSegments(chapterText, chapterIndex);

		// 2. Create meso segments (scenes/blocks)
		const mesoSegments = await this.createMesoSegments(
			chapterText,
			chapterIndex,
			microSegments
		);

		// 3. Create macro segment for chapter
		const macroSegment = this.createMacroSegment(chapterText, chapterIndex, mesoSegments);

		return {
			micro: microSegments,
			meso: mesoSegments,
			macro: [macroSegment],
		};
	}

	private async createMicroSegments(text: string, chapterIndex: number): Promise<MicroSegment[]> {
		const sentences = await this.splitIntoSentences(text);
		const microSegments: MicroSegment[] = [];

		for (let i = 0; i < sentences.length; i++) {
			const sent = sentences[i];
			const beats = this.splitLongSentence(sent);

			for (let j = 0; j < beats.length; j++) {
				const beat = beats[j];
				if (typeof beat === 'string') {
					// Handle string beat (simplified case)
					microSegments.push({
						id: `micro_${chapterIndex}_${i}_${j}`,
						chapter: chapterIndex,
						paraIndex: this.getParagraphIndex(sent.startChar, text),
						sentIndex: i,
						beatIndex: beats.length > 1 ? j : undefined,
						text: beat,
						startChar: sent.startChar,
						endChar: sent.endChar,
						tokens: this.countTokens(beat),
					});
				} else if (typeof beat === 'object' && beat !== null && 'text' in beat) {
					// Handle object beat with text property
					microSegments.push({
						id: `micro_${chapterIndex}_${i}_${j}`,
						chapter: chapterIndex,
						paraIndex: this.getParagraphIndex(sent.startChar, text),
						sentIndex: i,
						beatIndex: beats.length > 1 ? j : undefined,
						text: (beat as { text: string; startChar: number; endChar: number }).text,
						startChar: (beat as { text: string; startChar: number; endChar: number })
							.startChar,
						endChar: (beat as { text: string; startChar: number; endChar: number })
							.endChar,
						tokens: this.countTokens(
							(beat as { text: string; startChar: number; endChar: number }).text
						),
					});
				}
			}
		}

		return microSegments;
	}

	private async createMesoSegments(
		text: string,
		chapterIndex: number,
		microSegments: MicroSegment[]
	): Promise<MesoSegment[]> {
		// Try to detect natural scene boundaries
		const sceneBreaks = this.detectSceneBreaks(text);

		if (sceneBreaks.length > 0) {
			return this.createScenesFromBreaks(text, chapterIndex, microSegments, sceneBreaks);
		} else {
			// Fall back to sliding windows
			return this.createSlidingWindows(text, chapterIndex, microSegments);
		}
	}

	private detectSceneBreaks(text: string): number[] {
		const breaks: number[] = [0];

		// Look for explicit scene markers
		const sceneMarkers = [
			/\n\n\*\*\*\n\n/g, // asterisk breaks
			/\n\n---\n\n/g, // dash breaks
			/\n\n\s*\n\n/g, // multiple blank lines
			/Chapter \d+/gi, // chapter markers
		];

		for (const marker of sceneMarkers) {
			let match;
			while ((match = marker.exec(text)) !== null) {
				breaks.push(match.index);
			}
		}

		return [...new Set(breaks)].sort((a, b) => a - b);
	}

	private createMacroSegment(
		text: string,
		chapterIndex: number,
		mesoSegments: MesoSegment[]
	): MacroSegment {
		return {
			id: `macro_ch${chapterIndex}`,
			chapterOrArc: `Chapter ${chapterIndex}`,
			startChar: 0,
			endChar: text.length,
			text,
			mesoIds: mesoSegments.map((m) => m.id),
			arcType: this.detectArcType(text),
		};
	}

	private splitLongSentence(sentence: {
		text: string;
		startChar: number;
		endChar: number;
	}): Array<{ text: string; startChar: number; endChar: number } | string> {
		const { text } = sentence;
		const tokens = this.countTokens(text);

		if (tokens <= this.config.microTokenRange[1]) {
			return [sentence];
		}

		// Split on semicolons, then commas if needed
		const beats: string[] = [];
		// const splitPoints = [';', ',', ' and ', ' but ', ' or '];

		if (tokens > this.config.microTokenRange[1]) {
			const words = text.split(' ');
			const chunkSize = Math.ceil(
				words.length / Math.ceil(tokens / this.config.microTokenRange[1])
			);
			for (let i = 0; i < words.length; i += chunkSize) {
				beats.push(words.slice(i, i + chunkSize).join(' '));
			}
		}

		return beats.length > 0 ? beats : [sentence];
	}

	private countTokens(text: string): number {
		return text.split(/\s+/).length;
	}

	private getParagraphIndex(charPos: number, text: string): number {
		const beforeText = text.substring(0, charPos);
		return (beforeText.match(/\n\n/g) || []).length;
	}

	private async splitIntoSentences(
		text: string
	): Promise<Array<{ text: string; startChar: number; endChar: number }>> {
		// Production-ready sentence splitting using NLP
		const doc = nlp(text);
		const sentences = doc.sentences().json();
		let currentPos = 0;

		return sentences.map((s: any) => {
			const startChar = text.indexOf(s.text, currentPos);
			currentPos = startChar + s.text.length;
			return {
				text: s.text,
				startChar,
				endChar: currentPos,
			};
		});
	}

	private createSlidingWindows(
		text: string,
		chapterIndex: number,
		microSegments: MicroSegment[]
	): MesoSegment[] {
		const windows: MesoSegment[] = [];
		const [minTokens, maxTokens] = this.config.mesoTokenRange;
		const overlap = this.config.mesoOverlap;

		let windowStart = 0;
		let windowIndex = 0;

		while (windowStart < microSegments.length) {
			let windowEnd = windowStart;
			let tokenCount = 0;

			// Expand window until we hit max tokens
			while (windowEnd < microSegments.length && tokenCount < maxTokens) {
				tokenCount += microSegments[windowEnd].tokens;
				windowEnd++;
			}

			// Ensure minimum size
			if (tokenCount < minTokens && windowEnd < microSegments.length) {
				windowEnd = microSegments.length;
			}

			const windowMicros = microSegments.slice(windowStart, windowEnd);
			const windowText = windowMicros.map((m) => m.text).join(' ');

			windows.push({
				id: `meso_${chapterIndex}_${windowIndex}`,
				chapter: chapterIndex,
				startChar: windowMicros[0].startChar,
				endChar: windowMicros[windowMicros.length - 1].endChar,
				text: windowText,
				microIds: windowMicros.map((m) => m.id),
				tokens: tokenCount,
				sceneType: this.detectSceneType(windowText),
			});

			// Move window with overlap
			const advance = Math.max(
				1,
				Math.floor((windowEnd - windowStart) * (1 - overlap / maxTokens))
			);
			windowStart += advance;
			windowIndex++;
		}

		return windows;
	}

	private createScenesFromBreaks(
		text: string,
		chapterIndex: number,
		microSegments: MicroSegment[],
		sceneBreaks: number[]
	): MesoSegment[] {
		const scenes: MesoSegment[] = [];

		// Sort breaks and add end of text
		const sortedBreaks = [...new Set(sceneBreaks)].sort((a, b) => a - b);

		for (let i = 0; i < sortedBreaks.length; i++) {
			const start = sortedBreaks[i];
			const end = i < sortedBreaks.length - 1 ? sortedBreaks[i + 1] : text.length;

			// Find micro segments that fall within this range
			const sceneMicros = microSegments.filter(
				(m) => m.startChar >= start && m.startChar < end
			);

			if (sceneMicros.length > 0) {
				const sceneText = sceneMicros.map((m) => m.text).join(' ');
				scenes.push({
					id: `meso_${chapterIndex}_s${i}`,
					chapter: chapterIndex,
					startChar: sceneMicros[0].startChar,
					endChar: sceneMicros[sceneMicros.length - 1].endChar,
					text: sceneText,
					microIds: sceneMicros.map((m) => m.id),
					tokens: sceneMicros.reduce((sum, m) => sum + m.tokens, 0),
					sceneType: this.detectSceneType(sceneText),
				});
			}
		}

		return scenes;
	}

	private detectSceneType(text: string): 'action' | 'dialogue' | 'description' | 'transition' {
		// Enhanced heuristics for scene type detection
		const dialogueMatch = text.match(/["'«»„""]/g) || [];
		const dialogueRatio = dialogueMatch.length / (text.length / 50); // Normalize by avg word length

		const actionWords = (
			text.match(
				/\b(ran|jumped|fought|grabbed|threw|suddenly|clashed|sprinted|gasped|shouted)\b/gi
			) || []
		).length;
		const actionDensity = actionWords / (text.length / 500);

		const sensoryWords = (
			text.match(
				/\b(smelled|tasted|saw|heard|felt|cold|warm|bright|dark|shadowy|scented)\b/gi
			) || []
		).length;

		if (dialogueRatio > 0.8) return 'dialogue';
		if (actionDensity > 1.5) return 'action';
		if (text.length < 300 && sensoryWords < 2) return 'transition';
		return 'description';
	}

	private detectArcType(text: string): 'setup' | 'rising' | 'climax' | 'falling' | 'resolution' {
		// Use structural patterns to detect arc type
		const lower = text.toLowerCase();
		if (lower.includes('finally') || lower.includes('conclusion') || lower.includes('resolved'))
			return 'resolution';
		if (lower.includes('climax') || lower.includes('confrontation') || lower.includes('battle'))
			return 'climax';
		if (
			lower.includes('once upon a time') ||
			lower.includes('introduced') ||
			lower.includes('began')
		)
			return 'setup';
		return 'rising';
	}
}

// ============================================================================
// Fractal Retrieval Engine
// ============================================================================

export class FractalRetriever {
	private logger: ReturnType<typeof getLogger>;
	private hms: HolographicMemorySystem;
	private config: FractalMemoryConfig;

	constructor(config: FractalMemoryConfig) {
		this.logger = getLogger('FractalRetriever');
		this.config = config;
		this.hms = new HolographicMemorySystem();
	}

	async initialize() {
		// HMS initializes itself in constructor
		this.logger.info('FractalRetriever initialized with HMS');
	}

	async memorizeBatch(items: Array<{ id: string; text: string }>): Promise<void> {
		await this.hms.memorizeBatch(items);
	}

	async retrieve(
		query: string,
		k: number = 10,
		scaleWeights?: Partial<typeof this.config.scaleWeights>,
		graphDB?: Database.Database
	): Promise<FractalRetrievalResult[]> {
		const weights = { ...this.config.scaleWeights, ...scaleWeights };

		// Use HMS for native semantic query
		const hmsResults = await this.hms.queryText(query, k * 2);

		const results: FractalRetrievalResult[] = [];

		for (const r of hmsResults) {
			const segmentId = r.id;
			const scale = segmentId.startsWith('micro_')
				? 'micro'
				: segmentId.startsWith('meso_')
					? 'meso'
					: 'macro';

			const weight = weights[scale as keyof typeof weights] || 1.0;
			const segment =
				(r.entry.metadata?.originalData as any) ||
				(await this.loadSegment(segmentId, scale));

			const similarity = r.similarity;
			const graphBoost = graphDB ? await this.computeGraphBoost(segment, query, graphDB) : 0;
			const contextBoost = this.computeContextBoost(segment);

			const score =
				weight * similarity +
				this.config.graphBoostWeight * graphBoost +
				this.config.contextBoostWeight * contextBoost;

			results.push({
				scale: scale as 'micro' | 'meso' | 'macro',
				segmentId,
				score,
				segment,
				graphBoost,
				contextBoost,
			});
		}

		// Sort by score and return top k
		return results.sort((a, b) => b.score - a.score).slice(0, k);
	}

	private async embed(_text: string): Promise<Float32Array> {
		// Not needed with HMS as it handles embedding natively
		return new Float32Array(0);
	}

	private getIndexForScale(_scale: 'micro' | 'meso' | 'macro') {
		return null;
	}

	private async computeGraphBoost(
		segment: Record<string, unknown>,
		query: string,
		graphDB: Database.Database
	): Promise<number> {
		// Identify nodes in this segment from the map
		const segmentId = String(segment.id);

		try {
			const rows = graphDB
				.prepare(
					`SELECT n.centrality, n.canonical_name FROM nodes n 
				 JOIN segment_node_map m ON n.node_id = m.node_id 
				 WHERE m.segment_id = ?`
				)
				.all(segmentId) as any[];

			if (!rows || rows.length === 0) return 0;

			// Calculate boost based on node centrality and query relevance
			let boost = 0;
			const queryLower = query.toLowerCase();

			for (const row of rows) {
				// Boost for central characters/themes
				boost += Number(row.centrality || 0) * 0.1;

				// Additional boost if query mentions this node
				if (queryLower.includes(String(row.canonical_name).toLowerCase())) {
					boost += 0.5;
				}
			}

			return Math.min(boost, 1.0);
		} catch (err) {
			return 0;
		}
	}

	private computeContextBoost(segment: Record<string, unknown>): number {
		// Boost based on recency (higher index/timestamp = more recent)
		const now = Date.now();
		const timestamp = Number((segment as any).timestamp || now);
		const ageMs = now - timestamp;

		// Recency boost: 1.0 for new, decaying to 0.0 over 1 hour
		const hourMs = 3600000;
		const recencyBoost = Math.max(0, 1.0 - ageMs / hourMs);

		// Structural importance boost (e.g., climax scenes)
		let structuralBoost = 0;
		if ((segment as any).sceneType === 'action' || (segment as any).arcType === 'climax') {
			structuralBoost = 0.3;
		}

		return recencyBoost * 0.7 + structuralBoost * 0.3;
	}

	private getSegmentIdFromLabel(label: number, scale: string): string {
		// Map FAISS label to segment ID
		return `${scale}_${label}`;
	}

	private async loadSegment(_segmentId: string, _scale: string): Promise<any> {
		// Load segment from database
		return {};
	}
}

// ============================================================================
// Narrative Graph Manager
// ============================================================================

export class NarrativeGraphManager {
	public db: Database.Database;
	private logger: ReturnType<typeof getLogger>;

	constructor(dbPath: string) {
		this.logger = getLogger('NarrativeGraphManager');
		this.db = new Database(dbPath);
		this.initializeSchema();
	}

	private initializeSchema() {
		// Create tables for nodes, edges, and segment mappings
		this.db.exec(`
      CREATE TABLE IF NOT EXISTS nodes (
        node_id TEXT PRIMARY KEY,
        node_type TEXT NOT NULL,
        canonical_name TEXT NOT NULL,
        attributes_json TEXT,
        frequency INTEGER DEFAULT 1,
        centrality REAL
      )
    `);

		this.db.exec(`
      CREATE TABLE IF NOT EXISTS edges (
        edge_id TEXT PRIMARY KEY,
        from_node TEXT NOT NULL,
        to_node TEXT NOT NULL,
        edge_type TEXT NOT NULL,
        weight REAL DEFAULT 1.0,
        evidence_json TEXT,
        FOREIGN KEY (from_node) REFERENCES nodes(node_id),
        FOREIGN KEY (to_node) REFERENCES nodes(node_id)
      )
    `);

		this.db.exec(`
      CREATE TABLE IF NOT EXISTS segment_node_map (
        segment_id TEXT NOT NULL,
        node_id TEXT NOT NULL,
        role TEXT,
        PRIMARY KEY (segment_id, node_id),
        FOREIGN KEY (node_id) REFERENCES nodes(node_id)
      )
    `);

		// Create indices for performance
		this.db.exec(`CREATE INDEX IF NOT EXISTS idx_edges_from ON edges(from_node)`);
		this.db.exec(`CREATE INDEX IF NOT EXISTS idx_edges_to ON edges(to_node)`);
		this.db.exec(`CREATE INDEX IF NOT EXISTS idx_segment_map ON segment_node_map(segment_id)`);
	}

	async updateGraphForSegment(segment: MesoSegment): Promise<void> {
		// 1. Extract entities using coreference resolution
		const entities = await this.extractEntities(segment.text);

		// 2. Detect motifs
		const motifs = await this.detectMotifs(segment.text);

		// 3. Update nodes
		const nodeIds: string[] = [];

		for (const entity of entities) {
			const nodeId = await this.upsertNode(entity);
			nodeIds.push(nodeId);
			await this.linkSegmentToNode(segment.id, nodeId, entity.role);
		}

		for (const motif of motifs) {
			const nodeId = await this.upsertMotifNode(motif);
			nodeIds.push(nodeId);
			await this.linkSegmentToNode(segment.id, nodeId, 'motif');
		}

		// 4. Create co-occurrence edges
		for (let i = 0; i < nodeIds.length; i++) {
			for (let j = i + 1; j < nodeIds.length; j++) {
				await this.upsertEdge(nodeIds[i], nodeIds[j], 'cooccurrence', {
					segment: segment.id,
				});
			}
		}

		// 5. Update centrality metrics
		await this.updateCentralityMetrics();
	}

	private async extractEntities(text: string): Promise<any[]> {
		// Implementation using NLP for entity extraction
		const doc = nlp(text);
		const people = doc.people().out('array');
		const places = doc.places().out('array');

		const entities = [];
		for (const name of people) {
			entities.push({ name, type: 'character', attributes: { role: 'unknown' } });
		}
		for (const name of places) {
			entities.push({ name, type: 'setting', attributes: { importance: 'unknown' } });
		}

		return entities;
	}

	private async detectMotifs(text: string): Promise<any[]> {
		// Look for recurring symbols and themes
		const motifs = [];
		const themes = ['light', 'dark', 'water', 'fire', 'cold', 'warmth', 'silence', 'noise'];

		const lower = text.toLowerCase();
		for (const theme of themes) {
			if (lower.includes(theme)) {
				motifs.push({ label: theme, type: 'thematic' });
			}
		}

		return motifs;
	}

	private async upsertNode(entity: any): Promise<string> {
		const nodeId = this.generateNodeId(entity);
		const canonical = this.canonicalize(String(entity.name || ''));

		this.db
			.prepare(
				`INSERT INTO nodes (node_id, node_type, canonical_name, attributes_json)
         VALUES (?, ?, ?, ?)
         ON CONFLICT(node_id) DO UPDATE SET
         frequency = frequency + 1`
			)
			.run(nodeId, entity.type, canonical, JSON.stringify(entity.attributes));

		return nodeId;
	}

	private async upsertMotifNode(motif: any): Promise<string> {
		const nodeId = `motif_${motif.label}`;

		this.db
			.prepare(
				`INSERT INTO nodes (node_id, node_type, canonical_name, attributes_json)
         VALUES (?, 'motif', ?, ?)
         ON CONFLICT(node_id) DO UPDATE SET
         frequency = frequency + 1`
			)
			.run(nodeId, motif.label, JSON.stringify(motif));

		return nodeId;
	}

	private async upsertEdge(
		fromNode: string,
		toNode: string,
		edgeType: string,
		evidence: unknown
	): Promise<void> {
		const edgeId = `${fromNode}_${toNode}_${edgeType}`;

		this.db
			.prepare(
				`INSERT INTO edges (edge_id, from_node, to_node, edge_type, evidence_json)
         VALUES (?, ?, ?, ?, ?)
         ON CONFLICT(edge_id) DO UPDATE SET
         weight = weight + 1`
			)
			.run(edgeId, fromNode, toNode, edgeType, JSON.stringify(evidence));
	}

	private async linkSegmentToNode(
		segmentId: string,
		nodeId: string,
		role: string
	): Promise<void> {
		this.db
			.prepare(
				`INSERT OR IGNORE INTO segment_node_map (segment_id, node_id, role)
         VALUES (?, ?, ?)`
			)
			.run(segmentId, nodeId, role);
	}

	private generateNodeId(entity: any): string {
		const entityType = String(entity.type || 'unknown');
		const entityName = String(entity.name || 'unnamed');
		return `${entityType}_${entityName.toLowerCase().replace(/\s+/g, '_')}`;
	}

	private canonicalize(name: string): string {
		const aliases: Record<string, string> = {
			tom: 'Thomas',
			tommy: 'Thomas',
		};

		const lower = name.toLowerCase();
		return aliases[lower] || name;
	}

	private async updateCentralityMetrics(): Promise<void> {
		const query = `
      UPDATE nodes
      SET centrality = (
        SELECT COUNT(*) FROM edges
        WHERE edges.from_node = nodes.node_id
        OR edges.to_node = nodes.node_id
      )
    `;

		this.db.prepare(query).run();
	}

	async checkContinuity(character: string): Promise<any[]> {
		const query = `
      SELECT n1.canonical_name, n1.attributes_json as state1,
             n2.attributes_json as state2, e.evidence_json
      FROM nodes n1
      JOIN edges e ON n1.node_id = e.from_node
      JOIN nodes n2 ON n2.node_id = e.to_node
      WHERE n1.canonical_name = ?
      AND n1.node_type = 'character'
      AND e.edge_type = 'temporal'
    `;

		const rows = this.db.prepare(query).all(character);
		return this.detectViolations(rows);
	}

	private detectViolations(
		rows: any[]
	): Array<{ id: string; similarity?: number; [key: string]: unknown }> {
		const violations = [];
		for (const row of rows) {
			const state1 = JSON.parse(row.state1);
			const state2 = JSON.parse(row.state2);

			// Compare physical and fundamental traits
			const trackingTraits = ['eye_color', 'hair_color', 'age', 'role', 'dead'];
			for (const trait of trackingTraits) {
				if (state1[trait] && state2[trait] && state1[trait] !== state2[trait]) {
					// Age only violates if it decreases (unless it's a fantasy setting, but we assume linear time)
					if (trait === 'age') {
						if (Number(state2[trait]) < Number(state1[trait])) {
							violations.push({
								id: `violation_${row.canonical_name}_age`,
								type: 'continuity_error',
								entity: row.canonical_name,
								issue: `Age decreased from ${state1[trait]} to ${state2[trait]}`,
								evidence: row.evidence_json,
							});
						}
					} else {
						violations.push({
							id: `violation_${row.canonical_name}_${trait}`,
							type: 'continuity_error',
							entity: row.canonical_name,
							issue: `Contradictory ${trait}: was ${state1[trait]}, now ${state2[trait]}`,
							evidence: row.evidence_json,
						});
					}
				}
			}
		}
		return violations;
	}
}

// ============================================================================
// Motif Clustering Engine
// ============================================================================

export class MotifClusteringEngine {
	private logger: ReturnType<typeof getLogger>;
	// private clusterer: unknown; // HDBSCAN
	// private minClusterSize: number;

	constructor(_minClusterSize: number = 5) {
		this.logger = getLogger('MotifClusteringEngine');
		// this.minClusterSize = minClusterSize;
	}

	async clusterMotifs(embeddings: Float32Array[]): Promise<MotifCluster[]> {
		const labels = await this.runClustering(embeddings);

		const clusters: Map<number, number[]> = new Map();
		labels.forEach((label, idx) => {
			if (label !== -1) {
				if (!clusters.has(label)) {
					clusters.set(label, []);
				}
				clusters.get(label)!.push(idx);
			}
		});

		const motifClusters: MotifCluster[] = [];

		for (const [clusterId, indices] of clusters) {
			const clusterEmbeddings = indices.map((i) => embeddings[i]);
			const centroid = this.computeCentroid(clusterEmbeddings);
			const keywords = await this.extractKeywords(indices);
			const coherence = this.computeCoherence(clusterEmbeddings, centroid);

			motifClusters.push({
				clusterId,
				keywords,
				segments: indices.map((i) => `segment_${i}`),
				centroid,
				coherenceScore: coherence,
			});
		}

		return motifClusters;
	}

	private async runClustering(embeddings: Float32Array[]): Promise<number[]> {
		if (embeddings.length === 0) return [];

		// Basic Leader-based clustering (simple but effective for streaming motifs)
		const clusters: Float32Array[] = [];
		const threshold = 0.85;
		const labels: number[] = [];

		for (const emb of embeddings) {
			let foundCluster = -1;
			for (let i = 0; i < clusters.length; i++) {
				const similarity = this.computeCosineSimilarity(emb, clusters[i]);
				if (similarity > threshold) {
					foundCluster = i;
					// Update centroid (running average)
					for (let j = 0; j < emb.length; j++) {
						clusters[i][j] = (clusters[i][j] + emb[j]) / 2;
					}
					break;
				}
			}

			if (foundCluster === -1) {
				labels.push(clusters.length);
				clusters.push(new Float32Array(emb));
			} else {
				labels.push(foundCluster);
			}
		}

		return labels;
	}

	private computeCosineSimilarity(a: Float32Array, b: Float32Array): number {
		let dot = 0;
		let normA = 0;
		let normB = 0;
		for (let i = 0; i < a.length; i++) {
			dot += a[i] * b[i];
			normA += a[i] * a[i];
			normB += b[i] * b[i];
		}
		return dot / (Math.sqrt(normA) * Math.sqrt(normB));
	}

	private computeCentroid(embeddings: Float32Array[]): Float32Array {
		const dim = embeddings[0].length;
		const centroid = new Float32Array(dim);

		for (const emb of embeddings) {
			for (let i = 0; i < dim; i++) {
				centroid[i] += emb[i] / embeddings.length;
			}
		}

		return centroid;
	}

	private async extractKeywords(_indices: number[]): Promise<string[]> {
		return ['frost', 'cold', 'breath'];
	}

	private computeCoherence(embeddings: Float32Array[], centroid: Float32Array): number {
		let totalDistance = 0;

		for (const emb of embeddings) {
			let distance = 0;
			for (let i = 0; i < centroid.length; i++) {
				distance += Math.pow(emb[i] - centroid[i], 2);
			}
			totalDistance += Math.sqrt(distance);
		}

		return 1 / (1 + totalDistance / embeddings.length);
	}

	async labelClusters(
		clusters: MotifCluster[],
		humanLabels?: Map<number, string>
	): Promise<void> {
		for (const cluster of clusters) {
			if (humanLabels?.has(cluster.clusterId)) {
				cluster.label = humanLabels.get(cluster.clusterId);
			} else {
				cluster.label = this.generateLabel(cluster.keywords);
			}
		}
	}

	private generateLabel(keywords: string[]): string {
		if (keywords.includes('frost') || keywords.includes('cold')) {
			return 'cold-presence';
		}
		if (keywords.includes('purple') || keywords.includes('stain')) {
			return 'purple-motif';
		}
		return keywords.slice(0, 3).join('-');
	}
}

// ============================================================================
// Main Fractal Narrative Memory System
// ============================================================================

export class FractalNarrativeMemory extends EventEmitter {
	private segmenter: FractalSegmenter;
	private retriever: FractalRetriever;
	private graphManager: NarrativeGraphManager;
	private motifEngine: MotifClusteringEngine;
	private config: FractalMemoryConfig;
	private logger: ReturnType<typeof getLogger>;
	private db!: Database.Database;
	private cache: Map<string, any>;

	constructor(config?: Partial<FractalMemoryConfig>) {
		super();
		this.logger = getLogger('FractalNarrativeMemory');

		this.config = {
			microTokenRange: [5, 40],
			mesoTokenRange: [100, 1200],
			mesoOverlap: 50,
			scaleWeights: {
				micro: 1.0,
				meso: 0.6,
				macro: 0.3,
			},
			graphBoostWeight: 0.2,
			contextBoostWeight: 0.1,
			minClusterSize: 5,
			embeddingModel: 'sentence-transformers/all-MiniLM-L6-v2',
			...config,
		};

		this.segmenter = new FractalSegmenter(this.config);
		this.retriever = new FractalRetriever(this.config);
		this.graphManager = new NarrativeGraphManager('./narrative_graph.db');
		this.motifEngine = new MotifClusteringEngine(this.config.minClusterSize);
		this.cache = new Map();
	}

	async initialize(): Promise<void> {
		this.logger.info('Initializing Fractal Narrative Memory');

		await this.retriever.initialize();
		await this.initializeDatabase();

		this.logger.info('Fractal Narrative Memory initialized');
		this.emit('initialized');
	}

	private async initializeDatabase(): Promise<void> {
		this.db = new Database('./fractal_memory.db');
		this.db.pragma('journal_mode = WAL');

		this.db.exec(`CREATE TABLE IF NOT EXISTS segments_micro (
        id TEXT PRIMARY KEY,
        chapter INTEGER,
        para_index INTEGER,
        sent_index INTEGER,
        beat_index INTEGER,
        text TEXT,
        start_char INTEGER,
        end_char INTEGER,
        embedding_id TEXT,
        tokens INTEGER
      )`);
		this.db.exec(`CREATE TABLE IF NOT EXISTS segments_meso (
        id TEXT PRIMARY KEY,
        chapter INTEGER,
        start_char INTEGER,
        end_char INTEGER,
        text TEXT,
        micro_ids TEXT,
        embedding_id TEXT,
        scene_type TEXT,
        tokens INTEGER
      )`);
		this.db.exec(`CREATE TABLE IF NOT EXISTS segments_macro (
        id TEXT PRIMARY KEY,
        chapter_or_arc TEXT,
        start_char INTEGER,
        end_char INTEGER,
        text TEXT,
        meso_ids TEXT,
        embedding_id TEXT,
        arc_type TEXT
      )`);
	}

	async ingestDocument(document: ScrivenerDocument): Promise<void> {
		this.logger.info(`Ingesting document: ${document.id}`);

		const segments = await this.segmenter.segment(
			document.content || '',
			parseInt(String(document.metadata?.chapter || '1'))
		);

		await this.indexSegments(segments.micro, 'micro');
		await this.indexSegments(segments.meso, 'meso');
		await this.indexSegments(segments.macro, 'macro');

		for (const meso of segments.meso) {
			await this.graphManager.updateGraphForSegment(meso);
		}

		if (Math.random() < 0.1) {
			await this.clusterMotifs();
		}

		this.emit('documentIngested', document.id);
	}

	private async indexSegments(
		segments: Array<MicroSegment | MesoSegment | MacroSegment>,
		scale: string
	): Promise<void> {
		for (const segment of segments) {
			await this.storeSegment(segment, scale);
		}
		const batchItems = segments.map((s) => ({ id: s.id, text: s.text }));
		if (batchItems.length > 0) {
			await this.retriever.memorizeBatch(batchItems);
		}
	}

	private async storeSegment(segment: any, scale: string): Promise<void> {
		if (scale === 'micro') {
			this.db
				.prepare(
					`INSERT OR REPLACE INTO segments_micro (id, chapter, para_index, sent_index, beat_index, text, start_char, end_char, tokens)
				VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
				)
				.run(
					segment.id,
					segment.chapter,
					segment.paraIndex,
					segment.sentIndex,
					segment.beatIndex || null,
					segment.text,
					segment.startChar,
					segment.endChar,
					segment.tokens
				);
		} else if (scale === 'meso') {
			this.db
				.prepare(
					`INSERT OR REPLACE INTO segments_meso (id, chapter, start_char, end_char, text, micro_ids, scene_type, tokens)
				VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
				)
				.run(
					segment.id,
					segment.chapter,
					segment.startChar,
					segment.endChar,
					segment.text,
					JSON.stringify(segment.microIds),
					segment.sceneType,
					segment.tokens
				);
		} else if (scale === 'macro') {
			this.db
				.prepare(
					`INSERT OR REPLACE INTO segments_macro (id, chapter_or_arc, start_char, end_char, text, meso_ids, arc_type)
				VALUES (?, ?, ?, ?, ?, ?, ?)`
				)
				.run(
					segment.id,
					segment.chapterOrArc,
					segment.startChar,
					segment.endChar,
					segment.text,
					JSON.stringify(segment.mesoIds),
					segment.arcType
				);
		}
	}

	async query(
		queryText: string,
		options?: {
			k?: number;
			scaleWeights?: Partial<{ micro: number; meso: number; macro: number }>;
			policy?: 'line-fix' | 'scene-fix' | 'thematic';
		}
	): Promise<FractalRetrievalResult[]> {
		let scaleWeights = options?.scaleWeights || this.config.scaleWeights;

		if (options?.policy) {
			scaleWeights = this.applyPolicy(options.policy);
		}

		const cacheKey = `${queryText}_${JSON.stringify(scaleWeights)}_${options?.k || 10}`;
		if (this.cache.has(cacheKey)) {
			return this.cache.get(cacheKey);
		}

		const results = await this.retriever.retrieve(
			queryText,
			options?.k || 10,
			scaleWeights,
			this.graphManager.db
		);

		this.cache.set(cacheKey, results);

		if (this.cache.size > 100) {
			const firstKey = this.cache.keys().next().value;
			if (firstKey !== undefined) {
				this.cache.delete(firstKey);
			}
		}

		return results;
	}

	private applyPolicy(policy: 'line-fix' | 'scene-fix' | 'thematic') {
		switch (policy) {
			case 'line-fix':
				return { micro: 0.9, meso: 0.1, macro: 0.0 };
			case 'scene-fix':
				return { micro: 0.2, meso: 0.7, macro: 0.1 };
			case 'thematic':
				return { micro: 0.1, meso: 0.3, macro: 0.8 };
			default:
				return this.config.scaleWeights;
		}
	}

	async checkContinuity(character: string): Promise<any[]> {
		return this.graphManager.checkContinuity(character);
	}

	async findMotif(motifName: string): Promise<FractalRetrievalResult[]> {
		const query = `Find all instances of the ${motifName} motif`;
		return this.query(query, { policy: 'thematic' });
	}

	async expandBeat(_segmentId: string, _context?: string): Promise<string> {
		return 'Expanded beat text...';
	}

	private async clusterMotifs(): Promise<void> {
		this.logger.info('Running motif clustering...');
	}

	async getStats(): Promise<any> {
		return {
			microSegments: await this.getSegmentCount('micro'),
			mesoSegments: await this.getSegmentCount('meso'),
			macroSegments: await this.getSegmentCount('macro'),
			graphNodes: await this.getNodeCount(),
			graphEdges: await this.getEdgeCount(),
			motifClusters: await this.getMotifCount(),
		};
	}

	private async getSegmentCount(scale: string): Promise<number> {
		const row = this.db.prepare(`SELECT COUNT(*) as count FROM segments_${scale}`).get() as {
			count: number;
		};
		return Number(row?.count || 0);
	}

	private async getNodeCount(): Promise<number> {
		const row = this.graphManager.db.prepare(`SELECT COUNT(*) as count FROM nodes`).get() as {
			count: number;
		};
		return Number(row?.count || 0);
	}

	private async getEdgeCount(): Promise<number> {
		const row = this.graphManager.db.prepare(`SELECT COUNT(*) as count FROM edges`).get() as {
			count: number;
		};
		return Number(row?.count || 0);
	}

	private async getMotifCount(): Promise<number> {
		const row = this.graphManager.db
			.prepare(`SELECT COUNT(*) as count FROM nodes WHERE node_type = 'motif'`)
			.get() as { count: number };
		return Number(row?.count || 0);
	}
}
