/**
 * BRAIN Retrieval Operations — 3-layer pattern (search -> timeline -> fetch).
 *
 * Provides token-efficient memory access modeled after claude-mem's
 * search/timeline/get_observations workflow:
 *
 * 1. searchBrainCompact — lightweight index (IDs + titles, ~50 tokens/hit)
 * 2. timelineBrain      — chronological context around an anchor entry
 * 3. fetchBrainEntries  — full details for a filtered set of IDs
 * 4. observeBrain       — unified save (observations table)
 *
 * Wave 2A provides the core implementations; Wave 2B wires them through
 * the dispatch layer (engine-compat -> domain handler -> MCP gateway).
 *
 * @task T5131 T5132 T5133 T5134 T5135
 * @epic T5149
 */
import { createHash } from 'node:crypto';
import { getBrainAccessor } from '../store/brain-accessor.js';
import { typedAll } from '../store/typed-query.js';
import { embedText, isEmbeddingAvailable } from './brain-embedding.js';
import { searchBrain } from './brain-search.js';
// ============================================================================
// Layer 1: Compact Search
// ============================================================================
/**
 * Token-efficient compact search across BRAIN tables.
 * Returns index-level hits (~50 tokens per result).
 *
 * Delegates to searchBrain() from brain-search.ts for FTS5/LIKE search,
 * then projects results to a compact format with optional date filtering.
 *
 * @param projectRoot - Project root directory
 * @param params - Search parameters
 * @returns Compact search results with token estimate
 */
export async function searchBrainCompact(projectRoot, params) {
    const { query, limit, tables, dateStart, dateEnd } = params;
    if (!query || !query.trim()) {
        return { results: [], total: 0, tokensEstimated: 0 };
    }
    const searchResult = await searchBrain(projectRoot, query, {
        limit: limit ?? 10,
        tables,
    });
    // Project full results to compact format.
    // Note: searchBrain() returns rows from raw SQL (nativeDb) which use
    // snake_case column names, but the TypeScript types are camelCase.
    // We handle both naming conventions for robustness.
    let results = [];
    for (const d of searchResult.decisions) {
        const raw = d;
        results.push({
            id: d.id,
            type: 'decision',
            title: d.decision.slice(0, 80),
            date: (d.createdAt ?? raw['created_at']) || '',
        });
    }
    for (const p of searchResult.patterns) {
        const raw = p;
        results.push({
            id: p.id,
            type: 'pattern',
            title: p.pattern.slice(0, 80),
            date: (p.extractedAt ?? raw['extracted_at']) || '',
        });
    }
    for (const l of searchResult.learnings) {
        const raw = l;
        results.push({
            id: l.id,
            type: 'learning',
            title: l.insight.slice(0, 80),
            date: (l.createdAt ?? raw['created_at']) || '',
        });
    }
    for (const o of searchResult.observations) {
        const raw = o;
        results.push({
            id: o.id,
            type: 'observation',
            title: o.title.slice(0, 80),
            date: (o.createdAt ?? raw['created_at']) || '',
        });
    }
    // Apply date filters client-side if provided
    if (dateStart) {
        results = results.filter((r) => r.date >= dateStart);
    }
    if (dateEnd) {
        results = results.filter((r) => r.date <= dateEnd);
    }
    return {
        results,
        total: results.length,
        tokensEstimated: results.length * 50,
    };
}
// ============================================================================
// Layer 2: Timeline
// ============================================================================
/**
 * Determine the entry type from its ID prefix.
 *
 * Conventions:
 * - D... -> decision (D001, D-xxx)
 * - P... -> pattern  (P001, P-xxx)
 * - L... -> learning (L001, L-xxx)
 * - O... or CM-... -> observation (O-xxx, CM-xxx)
 */
function parseIdPrefix(id) {
    if (id.startsWith('D-') || /^D\d/.test(id))
        return 'decision';
    if (id.startsWith('P-') || /^P\d/.test(id))
        return 'pattern';
    if (id.startsWith('L-') || /^L\d/.test(id))
        return 'learning';
    if (id.startsWith('O-') || id.startsWith('O') || id.startsWith('CM-'))
        return 'observation';
    return null;
}
/**
 * Get chronological context around an anchor entry.
 * Fetches the anchor's full data, then queries all 4 BRAIN tables
 * via UNION ALL to find chronological neighbors.
 *
 * @param projectRoot - Project root directory
 * @param params - Timeline parameters with anchor ID and depth
 * @returns Anchor entry data with surrounding chronological entries
 */
export async function timelineBrain(projectRoot, params) {
    const { anchor: anchorId, depthBefore = 3, depthAfter = 3 } = params;
    // Ensure DB is initialized
    const { getBrainDb, getBrainNativeDb } = await import('../store/brain-sqlite.js');
    await getBrainDb(projectRoot);
    const nativeDb = getBrainNativeDb();
    if (!nativeDb) {
        return { anchor: null, before: [], after: [] };
    }
    // Determine anchor type and fetch it via accessor
    const anchorType = parseIdPrefix(anchorId);
    if (!anchorType) {
        return { anchor: null, before: [], after: [] };
    }
    const accessor = await getBrainAccessor(projectRoot);
    let anchorData = null;
    let anchorDate = null;
    switch (anchorType) {
        case 'decision': {
            const row = await accessor.getDecision(anchorId);
            if (row) {
                anchorData = row;
                anchorDate = row.createdAt;
            }
            break;
        }
        case 'pattern': {
            const row = await accessor.getPattern(anchorId);
            if (row) {
                anchorData = row;
                anchorDate = row.extractedAt;
            }
            break;
        }
        case 'learning': {
            const row = await accessor.getLearning(anchorId);
            if (row) {
                anchorData = row;
                anchorDate = row.createdAt;
            }
            break;
        }
        case 'observation': {
            const row = await accessor.getObservation(anchorId);
            if (row) {
                anchorData = row;
                anchorDate = row.createdAt;
            }
            break;
        }
    }
    if (!anchorData || !anchorDate) {
        return { anchor: null, before: [], after: [] };
    }
    // UNION ALL across all 4 tables to get chronological neighbors.
    // Excludes the anchor itself.
    const beforeRows = typedAll(nativeDb.prepare(`
    SELECT id, 'decision' AS type, created_at AS date FROM brain_decisions WHERE created_at < ? AND id != ?
    UNION ALL
    SELECT id, 'pattern' AS type, extracted_at AS date FROM brain_patterns WHERE extracted_at < ? AND id != ?
    UNION ALL
    SELECT id, 'learning' AS type, created_at AS date FROM brain_learnings WHERE created_at < ? AND id != ?
    UNION ALL
    SELECT id, 'observation' AS type, created_at AS date FROM brain_observations WHERE created_at < ? AND id != ?
    ORDER BY date DESC
    LIMIT ?
  `), anchorDate, anchorId, anchorDate, anchorId, anchorDate, anchorId, anchorDate, anchorId, depthBefore);
    const afterRows = typedAll(nativeDb.prepare(`
    SELECT id, 'decision' AS type, created_at AS date FROM brain_decisions WHERE created_at > ? AND id != ?
    UNION ALL
    SELECT id, 'pattern' AS type, extracted_at AS date FROM brain_patterns WHERE extracted_at > ? AND id != ?
    UNION ALL
    SELECT id, 'learning' AS type, created_at AS date FROM brain_learnings WHERE created_at > ? AND id != ?
    UNION ALL
    SELECT id, 'observation' AS type, created_at AS date FROM brain_observations WHERE created_at > ? AND id != ?
    ORDER BY date ASC
    LIMIT ?
  `), anchorDate, anchorId, anchorDate, anchorId, anchorDate, anchorId, anchorDate, anchorId, depthAfter);
    return {
        anchor: { id: anchorId, type: anchorType, data: anchorData },
        before: beforeRows.map((r) => ({ id: r.id, type: r.type, date: r.date })),
        after: afterRows.map((r) => ({ id: r.id, type: r.type, date: r.date })),
    };
}
// ============================================================================
// Layer 3: Batch Fetch
// ============================================================================
/**
 * Batch-fetch full details by IDs.
 * Groups IDs by prefix to query the correct tables via BrainDataAccessor.
 *
 * @param projectRoot - Project root directory
 * @param params - Fetch parameters with IDs
 * @returns Full entry data for each found ID, plus not-found list
 */
export async function fetchBrainEntries(projectRoot, params) {
    const { ids } = params;
    if (!ids || ids.length === 0) {
        return { results: [], notFound: [], tokensEstimated: 0 };
    }
    const accessor = await getBrainAccessor(projectRoot);
    // Group IDs by type prefix
    const decisionIds = [];
    const patternIds = [];
    const learningIds = [];
    const observationIds = [];
    const unknownIds = [];
    for (const id of ids) {
        const type = parseIdPrefix(id);
        switch (type) {
            case 'decision':
                decisionIds.push(id);
                break;
            case 'pattern':
                patternIds.push(id);
                break;
            case 'learning':
                learningIds.push(id);
                break;
            case 'observation':
                observationIds.push(id);
                break;
            default:
                unknownIds.push(id);
        }
    }
    const results = [];
    const notFound = [...unknownIds];
    // Fetch decisions
    for (const id of decisionIds) {
        const row = await accessor.getDecision(id);
        if (row) {
            results.push({ id, type: 'decision', data: row });
        }
        else {
            notFound.push(id);
        }
    }
    // Fetch patterns
    for (const id of patternIds) {
        const row = await accessor.getPattern(id);
        if (row) {
            results.push({ id, type: 'pattern', data: row });
        }
        else {
            notFound.push(id);
        }
    }
    // Fetch learnings
    for (const id of learningIds) {
        const row = await accessor.getLearning(id);
        if (row) {
            results.push({ id, type: 'learning', data: row });
        }
        else {
            notFound.push(id);
        }
    }
    // Fetch observations
    for (const id of observationIds) {
        const row = await accessor.getObservation(id);
        if (row) {
            results.push({ id, type: 'observation', data: row });
        }
        else {
            notFound.push(id);
        }
    }
    return {
        results,
        notFound,
        tokensEstimated: results.length * 500,
    };
}
// ============================================================================
// Observe (Unified Save)
// ============================================================================
/**
 * Keyword patterns for auto-classifying observation type from text.
 */
const TYPE_KEYWORDS = [
    { keywords: ['bug', 'fix', 'error', 'crash'], type: 'bugfix' },
    { keywords: ['refactor', 'rename', 'extract', 'move'], type: 'refactor' },
    { keywords: ['add', 'create', 'implement', 'new'], type: 'feature' },
    { keywords: ['decide', 'chose', 'pick', 'instead'], type: 'decision' },
    { keywords: ['update', 'change', 'modify', 'upgrade'], type: 'change' },
];
/**
 * Auto-classify observation type from text using keyword matching.
 */
function classifyObservationType(text) {
    const lower = text.toLowerCase();
    for (const { keywords, type } of TYPE_KEYWORDS) {
        for (const keyword of keywords) {
            if (lower.includes(keyword)) {
                return type;
            }
        }
    }
    return 'discovery';
}
/** Monotonic counter to prevent ID collisions within the same millisecond. */
let observeSeq = 0;
/**
 * Save an observation to the BRAIN observations table.
 * Replaces the external claude-mem save_observation pattern.
 *
 * Auto-classifies type from text if not provided. Generates a
 * unique ID with O- prefix + base36 timestamp.
 *
 * @param projectRoot - Project root directory
 * @param params - Observation data
 * @returns Created observation ID, type, and timestamp
 */
export async function observeBrain(projectRoot, params) {
    const { text, title: titleParam, type: typeParam, project, sourceSessionId, sourceType } = params;
    if (!text || !text.trim()) {
        throw new Error('Observation text is required');
    }
    const type = typeParam ?? classifyObservationType(text);
    const title = titleParam ?? text.slice(0, 120);
    const now = new Date().toISOString().replace('T', ' ').slice(0, 19);
    // Content-hash dedup: SHA-256 prefix of title+text
    const contentHash = createHash('sha256')
        .update(title + text)
        .digest('hex')
        .slice(0, 16);
    // Check for recent duplicate (same content within last 30 seconds)
    const { getBrainNativeDb } = await import('../store/brain-sqlite.js');
    const nativeDb = getBrainNativeDb();
    if (nativeDb) {
        const cutoff = new Date(Date.now() - 30000).toISOString().replace('T', ' ').slice(0, 19);
        const existing = typedAll(nativeDb.prepare('SELECT id, type, created_at FROM brain_observations WHERE content_hash = ? AND created_at > ?'), contentHash, cutoff);
        if (existing.length > 0) {
            return {
                id: existing[0].id,
                type: existing[0].type,
                createdAt: existing[0].created_at,
            };
        }
    }
    const id = `O-${Date.now().toString(36)}-${(observeSeq++ % 1000).toString(36)}`;
    const accessor = await getBrainAccessor(projectRoot);
    const row = await accessor.addObservation({
        id,
        type,
        title,
        narrative: text,
        contentHash,
        project: project ?? null,
        sourceSessionId: sourceSessionId ?? null,
        sourceType: sourceType ?? 'agent',
        createdAt: now,
    });
    // Populate embedding if provider is available (T5387).
    // Fire-and-forget: embedding runs in the background so it never blocks the CLI.
    // The observation is already saved above — if embedding fails, the observation
    // still exists, just without vector similarity search capability. (T027)
    if (isEmbeddingAvailable()) {
        setImmediate(() => {
            embedText(text)
                .then((vector) => {
                if (vector && nativeDb) {
                    nativeDb
                        .prepare('INSERT OR REPLACE INTO brain_embeddings (id, embedding) VALUES (?, ?)')
                        .run(id, Buffer.from(vector.buffer));
                }
            })
                .catch(() => {
                // Silently skip embedding failures — observation is already persisted
            });
        });
    }
    // Regenerate memory bridge for high-value observation types (T5240).
    // Only learning and decision types trigger bridge refresh to avoid excessive writes.
    if (type === 'decision') {
        import('./memory-bridge.js')
            .then(({ refreshMemoryBridge }) => refreshMemoryBridge(projectRoot))
            .catch(() => {
            /* Memory bridge refresh is best-effort */
        });
    }
    return {
        id: row.id,
        type: row.type,
        createdAt: row.createdAt,
    };
}
/**
 * Backfill embeddings for existing observations that lack them.
 *
 * Iterates through observations not yet in brain_embeddings and
 * generates vectors using the registered embedding provider.
 * Processes in batches to avoid memory pressure.
 *
 * @param projectRoot - Project root directory
 * @param options - Optional batch size configuration
 * @returns Count of processed and skipped observations
 */
export async function populateEmbeddings(projectRoot, options) {
    if (!isEmbeddingAvailable()) {
        return { processed: 0, skipped: 0 };
    }
    const { getBrainDb, getBrainNativeDb } = await import('../store/brain-sqlite.js');
    await getBrainDb(projectRoot);
    const nativeDb = getBrainNativeDb();
    if (!nativeDb) {
        return { processed: 0, skipped: 0 };
    }
    const batchSize = options?.batchSize ?? 50;
    let processed = 0;
    let skipped = 0;
    // Find observations without embeddings
    const rows = typedAll(nativeDb.prepare(`
    SELECT o.id, o.narrative, o.title
    FROM brain_observations o
    LEFT JOIN brain_embeddings e ON o.id = e.id
    WHERE e.id IS NULL AND o.narrative IS NOT NULL
    ORDER BY o.created_at DESC
  `));
    for (let i = 0; i < rows.length; i += batchSize) {
        const batch = rows.slice(i, i + batchSize);
        for (const row of batch) {
            try {
                const vector = await embedText(row.narrative || row.title);
                if (vector) {
                    nativeDb
                        .prepare('INSERT OR REPLACE INTO brain_embeddings (id, embedding) VALUES (?, ?)')
                        .run(row.id, Buffer.from(vector.buffer));
                    processed++;
                }
                else {
                    skipped++;
                }
            }
            catch {
                skipped++;
            }
        }
    }
    return { processed, skipped };
}
//# sourceMappingURL=brain-retrieval.js.map