/**
 * Event Processor — source-agnostic event capture processing
 *
 * Accepts a common MeetingPayload from any capture source (desktop client,
 * external integrations, calendar, manual) and writes an `event` entity
 * with `type: meeting` (the default). Handles:
 * 1. Entity creation with proper frontmatter
 * 2. Participant resolution to existing person entities
 * 3. LLM-powered summary generation from transcript
 * 4. Structured extraction (action items, decisions)
 * 5. Task entity creation from action items
 * 6. Body template population
 *
 * Today the only event capture surface is meeting recording, so the
 * payload type is still MeetingPayload — generalizing the payload shape
 * is a follow-up when/if other event sources land.
 */
import { completeSimple, getModel } from '@mariozechner/pi-ai';
import { loadUserConfig, resolveModelId, resolveApiKey, resolveProvider } from '../core/user-config.js';
import { log } from '../utils/log.js';
// ── Processor ─────────────────────────────────────────────────────────
export class EventProcessor {
    workspaceManager;
    wsHub;
    yjsManager;
    constructor(workspaceManager, wsHub, yjsManager) {
        this.workspaceManager = workspaceManager;
        this.wsHub = wsHub;
        this.yjsManager = yjsManager;
    }
    /**
     * Process a meeting payload: create entity, resolve participants,
     * generate summary asynchronously.
     */
    async process(payload, repo, user) {
        const graph = this.workspaceManager.getGraph(repo);
        const entityId = this.generateEntityId(payload);
        // Resolve participants to wikilinks
        const attendeeLinks = await this.resolveParticipants(payload.participants, repo);
        // Build frontmatter
        const now = new Date().toISOString();
        const data = {
            entity_type: 'event',
            entity_id: entityId,
            type: 'meeting',
            name: payload.title || `Meeting ${payload.startedAt.slice(0, 10)}`,
            date: payload.startedAt.slice(0, 10),
            attendees: attendeeLinks,
            created_at: now,
            updated_at: now,
            created_by: user?.id ?? payload.source,
            updated_by: user?.id ?? payload.source,
            source_ref: `${payload.source}:${payload.sourceId}`,
            tags: ['auto-transcribed', payload.platform].filter(Boolean),
            processing: true,
        };
        if (payload.durationMinutes) {
            data.duration = payload.durationMinutes >= 60
                ? `${Math.round(payload.durationMinutes / 60 * 10) / 10}h`
                : `${payload.durationMinutes}m`;
        }
        if (payload.meetingUrl)
            data.location = payload.meetingUrl;
        // Initial content (memo only, summary comes async)
        let content = '';
        if (payload.memo) {
            content = `## Notes\n\n${payload.memo}`;
        }
        // Create the entity immediately
        await graph.create({
            entityType: 'event',
            entityId,
            data,
            content,
            commitMessage: `Add event: ${data.name} (via ${payload.source})`,
            user,
        });
        this.broadcast('created', repo, entityId, user);
        // Generate summary asynchronously
        this.processAsync(payload, repo, entityId, attendeeLinks, user).catch(err => {
            log.warn(`[event-processor] Background processing failed for ${entityId}:`, err.message);
        });
        return {
            entityId,
            repo,
            url: `/${repo}/event/${entityId}`,
            attendeeLinks,
            extractionPending: true,
            tasksCreated: [],
        };
    }
    /**
     * Process a meeting for an entity that already exists (created by the web app).
     * Meeting capture UI flow: entity was created when recording started,
     * user typed notes in it, now we add the transcript and generate the summary.
     */
    async processExisting(payload, repo, existingEntityId, user) {
        const graph = this.workspaceManager.getGraph(repo);
        // Verify entity exists
        try {
            graph.get('event', existingEntityId);
        }
        catch {
            throw new Error(`Event entity not found: ${existingEntityId}`);
        }
        // Resolve participants
        const attendeeLinks = await this.resolveParticipants(payload.participants, repo);
        // Update entity with processing flag and attendees
        const updateData = {
            processing: true,
            attendees: attendeeLinks,
            updated_at: new Date().toISOString(),
        };
        if (payload.durationMinutes) {
            updateData.duration = payload.durationMinutes >= 60
                ? `${Math.round(payload.durationMinutes / 60 * 10) / 10}h`
                : `${payload.durationMinutes}m`;
        }
        if (payload.platform)
            updateData.tags = ['auto-transcribed', payload.platform];
        await graph.update({
            entityType: 'event',
            entityId: existingEntityId,
            data: updateData,
            content: undefined, // Don't overwrite content — keep user's notes until summary is ready
            commitMessage: `Start processing event: ${existingEntityId}`,
            user,
        });
        this.broadcast('updated', repo, existingEntityId, user);
        // Generate summary asynchronously (same pipeline as new events)
        this.processAsync(payload, repo, existingEntityId, attendeeLinks, user).catch(async (err) => {
            log.warn(`[event-processor] Background processing failed for ${existingEntityId}:`, err.message);
            // Always clear the processing flag so the entity doesn't stay stuck
            try {
                const g = this.workspaceManager.getGraph(repo);
                await g.update({
                    entityType: 'event',
                    entityId: existingEntityId,
                    data: { processing: false },
                    commitMessage: `Clear processing flag after error: ${existingEntityId}`,
                    user,
                });
                this.broadcast('updated', repo, existingEntityId, user);
            }
            catch { /* non-fatal */ }
        });
        return {
            entityId: existingEntityId,
            repo,
            url: `/${repo}/event/${existingEntityId}`,
            attendeeLinks,
            extractionPending: true,
            tasksCreated: [],
        };
    }
    /**
     * Async processing: LLM summary, extraction, task creation
     */
    async processAsync(payload, repo, entityId, attendeeLinks, user) {
        const graph = this.workspaceManager.getGraph(repo);
        // Format transcript
        const transcript = payload.transcriptText
            ?? (payload.transcript ? this.formatTranscript(payload.transcript) : '');
        // Use existing summary from source, or generate via LLM. Pass natural
        // names instead of wikilinks so the model can attribute statements to
        // specific attendees from contextual cues — the transcript only carries
        // channel-level "Me / Them" labels, but the LLM can recover names from
        // in-dialog mentions ("Sarah, what do you think?"), self-introductions,
        // and the calendar attendee list.
        const selfName = payload.participants.find(p => p.isSelf)?.name || 'Me';
        const remoteNames = payload.participants
            .filter(p => !p.isSelf)
            .map(p => p.name);
        let summary = payload.existingSummary ?? null;
        let summaryError;
        if (!summary && transcript) {
            const result = await this.generateSummary(transcript, payload.title || '', payload.memo || '', selfName, remoteNames);
            summary = result.summary;
            summaryError = result.error;
        }
        if (!summary) {
            // Fallback: raw transcript (or just memo if no transcript). Surface
            // the LLM error inline so the user sees *why* they got a raw transcript
            // instead of a generated summary — otherwise this fallback looks like
            // success and the actual failure is buried in server logs.
            const errorBanner = summaryError
                ? `> [!warning] AI summary failed\n> ${summaryError.replace(/\n/g, '\n> ')}`
                : null;
            const fallbackContent = [
                errorBanner,
                transcript || null,
                payload.memo ? `## Notes\n\n${payload.memo}` : null,
            ].filter(Boolean).join('\n\n---\n\n');
            await graph.update({
                entityType: 'event',
                entityId,
                data: { processing: false },
                // Only overwrite content if we have something to write
                content: fallbackContent || undefined,
                commitMessage: `Add transcript for event: ${payload.title}`,
                user,
                source: fallbackContent ? 'server-write' : undefined,
            });
            this.broadcast('updated', repo, entityId, { id: 'ai-summary', name: 'AI Summary', email: 'ai@studiograph.local' });
            return;
        }
        // Extract structured data
        const { actionItems, decisions } = await this.extractStructuredData(summary);
        // Create task entities from action items
        const tasksCreated = [];
        for (const item of actionItems) {
            try {
                const taskId = await this.createTask(item, entityId, repo, user);
                if (taskId)
                    tasksCreated.push(taskId);
            }
            catch {
                // Non-fatal — log and continue
            }
        }
        // Build final content
        const updateData = { processing: false };
        let finalContent = summary;
        if (decisions.length > 0) {
            finalContent += `\n\n## Decisions\n\n${decisions.map(d => `- ${d}`).join('\n')}`;
        }
        if (tasksCreated.length > 0) {
            finalContent += `\n\n## Action Items\n\n${tasksCreated.map(id => `- [[${id}]]`).join('\n')}`;
        }
        if (payload.memo) {
            finalContent += `\n\n---\n\n## Notes\n\n${payload.memo}`;
        }
        if (transcript) {
            finalContent += `\n\n---\n\n## Transcript\n\n${transcript}`;
        }
        await graph.update({
            entityType: 'event',
            entityId,
            data: updateData,
            content: finalContent,
            commitMessage: `Generate event notes: ${payload.title}`,
            user,
            source: 'server-write',
        });
        this.broadcast('updated', repo, entityId, { id: 'ai-summary', name: 'AI Summary', email: 'ai@studiograph.local' });
    }
    /**
     * Create a task entity from an action item string, linked to the event.
     */
    async createTask(actionItem, eventEntityId, repo, user) {
        const graph = this.workspaceManager.getGraph(repo);
        const slug = this.slugify(actionItem.slice(0, 60));
        const taskId = `task-${slug}-${Date.now()}`;
        const now = new Date().toISOString();
        await graph.create({
            entityType: 'task',
            entityId: taskId,
            data: {
                entity_type: 'task',
                entity_id: taskId,
                name: actionItem,
                status: 'open',
                date: now.slice(0, 10),
                created_at: now,
                updated_at: now,
                created_by: user?.id ?? 'event-processor',
                updated_by: user?.id ?? 'event-processor',
                tags: ['from-event'],
            },
            content: `## Description\n\n${actionItem}\n\n## Source\n\n- Event: [[${eventEntityId}]]\n`,
            commitMessage: `Create task from event: ${actionItem.slice(0, 50)}`,
            user,
        });
        return taskId;
    }
    // ── Helpers ──────────────────────────────────────────────────────────
    generateEntityId(payload) {
        const date = payload.startedAt.slice(0, 10);
        const title = payload.title || 'event';
        return `${date}-${this.slugify(title)}`;
    }
    async resolveParticipants(participants, repo) {
        const wikilinks = [];
        for (const p of participants) {
            const slug = this.slugify(p.name);
            // Try direct ID lookup
            try {
                const graph = this.workspaceManager.getGraph(repo);
                graph.get('person', slug);
                wikilinks.push(`[[${slug}]]`);
                continue;
            }
            catch { /* not found */ }
            // Try search
            try {
                const results = await this.workspaceManager.search({
                    query: p.name,
                    repoNames: [repo],
                    limit: 3,
                });
                const match = results.find(r => r.repoName === repo && r.data?.entity_type === 'person');
                if (match) {
                    wikilinks.push(`[[${match.data.entity_id}]]`);
                    continue;
                }
            }
            catch { /* search failed */ }
            // Fallback: unresolved wikilink
            wikilinks.push(`[[${slug}]]`);
        }
        return wikilinks;
    }
    formatTranscript(segments) {
        const blocks = [];
        let currentSpeaker = '';
        let currentTexts = [];
        for (const seg of segments) {
            if (seg.isFinal === false)
                continue;
            if (seg.speaker !== currentSpeaker) {
                if (currentSpeaker && currentTexts.length > 0) {
                    blocks.push(`**${currentSpeaker}:** ${currentTexts.join(' ')}`);
                }
                currentSpeaker = seg.speaker;
                currentTexts = [];
            }
            currentTexts.push(seg.text);
        }
        if (currentSpeaker && currentTexts.length > 0) {
            blocks.push(`**${currentSpeaker}:** ${currentTexts.join(' ')}`);
        }
        return blocks.join('\n\n');
    }
    async generateSummary(transcript, title, memo, selfName, remoteNames) {
        const { provider, modelId, apiKey } = this.getModelConfig();
        if (!apiKey) {
            return { summary: null, error: `No API key configured (provider=${provider}, model=${modelId})` };
        }
        try {
            const model = getModel(provider, modelId);
            const remoteNameList = remoteNames.length > 0 ? remoteNames.join(', ') : '(none on calendar)';
            const prompt = `You are a meeting notes assistant. Generate structured meeting notes from this transcript.

Meeting: ${title}
Note-taker (the "Me" channel): ${selfName}
Other attendees from calendar: ${remoteNameList}
${memo ? `Note-taker's typed notes: ${memo}` : ''}

Transcript:
${transcript.slice(0, 30000)}

The transcript labels are channel-based: "**Me:**" is ${selfName}; "**Them:**" is whichever remote attendee was speaking (multiple remote attendees share the same label, since they all came through the same audio channel).

Use contextual cues to attribute statements to specific people by name when you're reasonably confident — names mentioned in dialog ("Sarah, what do you think?"), self-introductions ("Hi, I'm Marcus"), domain expertise references, and the calendar attendee list above. When you genuinely can't tell which attendee said something, write generically ("an attendee asked…", "the team discussed…") rather than guess. Never fabricate attribution.

Write structured meeting notes in Markdown:
1. Brief 2–3 sentence overview
2. ### section headers for each major topic
3. Bullet points for key points, decisions, context — attribute to attendees by name where context permits
4. End with ### Next Steps listing concrete action items (include assignee name if identifiable)

Guidelines:
- Third person, past tense
- Concise but capture the substance
- Preserve important quotes when relevant
- Group related points under clear headers
- State decisions clearly

Return ONLY the Markdown notes.`;
            const response = await completeSimple(model, { messages: [{ role: 'user', content: prompt, timestamp: Date.now() }] }, { apiKey, maxTokens: 4096 });
            const textContent = response.content.find(c => c.type === 'text');
            const text = textContent?.type === 'text' ? textContent.text : '';
            const trimmed = text.trim();
            if (!trimmed) {
                return { summary: null, error: `LLM returned empty response (provider=${provider}, model=${modelId})` };
            }
            return { summary: trimmed };
        }
        catch (err) {
            const message = err.message || String(err);
            log.warn('[event-processor] LLM summary failed:', message);
            return { summary: null, error: `LLM call failed (provider=${provider}, model=${modelId}): ${message}` };
        }
    }
    async extractStructuredData(summary) {
        const { provider, modelId, apiKey } = this.getModelConfig();
        if (!apiKey)
            return { actionItems: [], decisions: [] };
        try {
            const model = getModel(provider, modelId);
            const response = await completeSimple(model, {
                messages: [{
                        role: 'user',
                        content: `Extract action items and decisions from these meeting notes. Return JSON only:
{"actionItems": ["item with assignee if mentioned"], "decisions": ["decision made"]}

Notes:
${summary.slice(0, 8000)}`,
                        timestamp: Date.now(),
                    }],
            }, { apiKey, maxTokens: 1024 });
            const textContent = response.content.find(c => c.type === 'text');
            const text = textContent?.type === 'text' ? textContent.text : '';
            const parsed = JSON.parse(text);
            return {
                actionItems: Array.isArray(parsed.actionItems) ? parsed.actionItems : [],
                decisions: Array.isArray(parsed.decisions) ? parsed.decisions : [],
            };
        }
        catch {
            return { actionItems: [], decisions: [] };
        }
    }
    getModelConfig() {
        const userConfig = loadUserConfig();
        const provider = resolveProvider(userConfig);
        const modelId = resolveModelId(userConfig);
        const apiKey = resolveApiKey(userConfig);
        return { provider, modelId, apiKey };
    }
    broadcast(action, repo, entityId, user) {
        this.wsHub?.broadcast({
            type: 'entity_change',
            action,
            repo,
            entityType: 'event',
            entityId,
            actor: user?.name ?? 'system',
            source: 'api',
            timestamp: new Date().toISOString(),
        });
    }
    slugify(str) {
        return str
            .toLowerCase()
            .replace(/[^a-z0-9]+/g, '-')
            .replace(/^-|-$/g, '')
            .slice(0, 80);
    }
}
//# sourceMappingURL=event-processor.js.map