import { EventEmitter } from 'events';
import { createReadStream, existsSync, readFileSync, readdirSync, statSync } from 'fs';
import { basename, join } from 'path';
import { createInterface } from 'readline';
import chokidar from 'chokidar';
import { resolveJsonlFilePath } from './agent-title-match.js';
import { canonicalizeImagePaths } from './media-path.js';
import { cleanUserText, filterAssistantJsonlParts, isNoiseChatText, parseUserImagePaths, stripJsonlRedactionArtifacts, } from './message-filter.js';
import { bridgePipelineLog } from './history-pipeline-log.js';
const JSONL_POLL_MS = parseInt(process.env.JSONL_POLL_MS ?? '120', 10);
/** Debounce only for non–live-watched files (index rebuild path). */
const JSONL_EMIT_DEBOUNCE_MS = parseInt(process.env.JSONL_EMIT_DEBOUNCE_MS ?? '0', 10);
const JSONL_WRITE_STABILITY_MS = parseInt(process.env.JSONL_WRITE_STABILITY_MS ?? '50', 10);
export class JsonlIndex extends EventEmitter {
    projectsDir;
    debounceTimer = null;
    subscribed = new Map();
    /** After phone `agents:unsubscribe` — keep JSONL watch until DOM overlay drains into archive. */
    draining = new Map();
    mtimeByAgent = new Map();
    pollTimer = null;
    fileCache = new Map();
    emitFileTimers = new Map();
    liveWatcher = null;
    liveWatchKey = '';
    /** Skip poll `agent:history` while serving explicit `agents:history` (avoids relay race). */
    historyReplyInFlight = new Set();
    bridgeState = () => ({ tabs: [] });
    constructor(projectsDir) {
        super();
        this.projectsDir = projectsDir;
    }
    setBridgeStateProvider(fn) {
        this.bridgeState = fn;
    }
    resolveOpts(title) {
        const state = this.bridgeState();
        const activeTab = state.tabs?.find((t) => t.active);
        return {
            title,
            composerIdByTitle: state.composerIdByTitle,
            activeComposerId: state.activeComposerId,
            activeTabTitle: activeTab?.title,
        };
    }
    start() {
        void this.rebuild();
        chokidar
            .watch(this.projectsDir, {
            ignoreInitial: true,
            depth: 8,
            awaitWriteFinish: {
                stabilityThreshold: JSONL_WRITE_STABILITY_MS,
                pollInterval: 50,
            },
            ignored: (p) => shouldIgnoreWatchPath(p),
        })
            .on('all', (_event, filePath) => {
            if (!filePath || !filePath.endsWith('.jsonl'))
                return;
            this.scheduleRebuild();
            this.scheduleEmitHistoryForFile(filePath);
        });
        this.pollTimer = setInterval(() => void this.pollSubscribed(), JSONL_POLL_MS);
    }
    stop() {
        if (this.pollTimer)
            clearInterval(this.pollTimer);
        void this.liveWatcher?.close();
        this.liveWatcher = null;
        this.liveWatchKey = '';
    }
    subscribe(agentId, title, opts) {
        this.subscribed.set(agentId, { title });
        this.refreshLiveWatcher();
        if (opts?.emitHistory !== false) {
            void this.emitHistoryForAgent(agentId, title);
        }
    }
    unsubscribe(agentId) {
        this.subscribed.delete(agentId);
        if (!this.draining.has(agentId))
            this.mtimeByAgent.delete(agentId);
        this.refreshLiveWatcher();
    }
    /** Background JSONL sync after unsubscribe while DOM overlay is still pending. */
    drain(agentId, title) {
        const prev = this.draining.get(agentId);
        this.draining.set(agentId, { title: title ?? prev?.title });
        this.refreshLiveWatcher();
        void this.emitHistoryForAgent(agentId, title ?? prev?.title);
    }
    stopDrain(agentId) {
        this.draining.delete(agentId);
        this.mtimeByAgent.delete(agentId);
        this.refreshLiveWatcher();
    }
    getDrainingAgents() {
        return this.draining;
    }
    agentsWatchedForJsonl() {
        const out = new Map();
        for (const [id, meta] of this.subscribed)
            out.set(id, meta);
        for (const [id, meta] of this.draining) {
            if (!out.has(id))
                out.set(id, meta);
        }
        return out;
    }
    /** Immediate `change` on subscribed/draining `.jsonl` (no awaitWriteFinish delay). */
    refreshLiveWatcher() {
        const paths = [];
        for (const [agentId, meta] of this.agentsWatchedForJsonl()) {
            const p = resolveJsonlFilePath(this.projectsDir, agentId, this.resolveOpts(meta.title));
            if (p)
                paths.push(p);
        }
        const key = paths.sort().join('\0');
        if (key === this.liveWatchKey)
            return;
        this.liveWatchKey = key;
        void this.liveWatcher?.close();
        this.liveWatcher = null;
        if (!paths.length)
            return;
        this.liveWatcher = chokidar.watch(paths, { ignoreInitial: true, persistent: true });
        const onLive = (filePath) => {
            if (!filePath.endsWith('.jsonl'))
                return;
            void this.emitHistoryForFileNow(filePath);
        };
        this.liveWatcher.on('change', onLive);
        this.liveWatcher.on('add', onLive);
    }
    getSubscribedAgents() {
        return this.subscribed;
    }
    async pollSubscribed() {
        for (const [agentId, meta] of this.agentsWatchedForJsonl()) {
            const filePath = resolveJsonlFilePath(this.projectsDir, agentId, this.resolveOpts(meta.title));
            if (!filePath)
                continue;
            try {
                const mtime = statSync(filePath).mtimeMs;
                const prev = this.mtimeByAgent.get(agentId);
                if (prev === mtime)
                    continue;
                this.mtimeByAgent.set(agentId, mtime);
                void this.emitHistoryForFileNow(filePath, agentId);
            }
            catch {
                /* skip */
            }
        }
    }
    async emitHistoryForAgent(agentId, title) {
        const filePath = resolveJsonlFilePath(this.projectsDir, agentId, this.resolveOpts(title));
        if (!filePath) {
            this.emit('agent:history', {
                agentId,
                messages: [],
                totalMessages: 0,
                updatedAt: Date.now(),
            });
            return;
        }
        try {
            this.mtimeByAgent.set(agentId, statSync(filePath).mtimeMs);
        }
        catch {
            /* ignore */
        }
        await this.emitHistoryForFileNow(filePath, agentId);
    }
    scheduleEmitHistoryForFile(filePath, replyAgentId) {
        if (this.isLiveWatchedFile(filePath)) {
            void this.emitHistoryForFileNow(filePath, replyAgentId);
            return;
        }
        if (JSONL_EMIT_DEBOUNCE_MS <= 0) {
            void this.emitHistoryForFileNow(filePath, replyAgentId);
            return;
        }
        const key = `${filePath}\0${replyAgentId ?? ''}`;
        const prev = this.emitFileTimers.get(key);
        if (prev)
            clearTimeout(prev);
        this.emitFileTimers.set(key, setTimeout(() => {
            this.emitFileTimers.delete(key);
            void this.emitHistoryForFileNow(filePath, replyAgentId);
        }, JSONL_EMIT_DEBOUNCE_MS));
    }
    isLiveWatchedFile(filePath) {
        if (!this.liveWatchKey)
            return false;
        return this.liveWatchKey.split('\0').includes(filePath);
    }
    async loadMessagesForFile(filePath) {
        const st = statSync(filePath);
        const cached = this.fileCache.get(filePath);
        if (cached && cached.size === st.size)
            return cached.messages;
        if (cached && st.size > cached.size) {
            const tail = await parseJsonlTail(filePath, cached.lineNo);
            const messages = [...cached.messages, ...tail.appended];
            this.fileCache.set(filePath, {
                lineNo: tail.lineNo,
                messages,
                size: st.size,
            });
            return messages;
        }
        const full = await parseJsonlFileFull(filePath);
        this.fileCache.set(filePath, {
            lineNo: full.lineNo,
            messages: full.messages,
            size: st.size,
        });
        return full.messages;
    }
    async emitHistoryForFileNow(filePath, replyAgentId) {
        const fileAgentId = basename(filePath, '.jsonl');
        if (!fileAgentId || fileAgentId.includes('/'))
            return;
        const agentId = replyAgentId ?? fileAgentId;
        try {
            const messages = await this.loadMessagesForFile(filePath);
            const rawLineNo = this.fileCache.get(filePath)?.lineNo ?? messages.length;
            this.mtimeByAgent.set(agentId, statSync(filePath).mtimeMs);
            this.emit('agent:jsonl:updated', {
                agentId: fileAgentId,
                messages,
                totalMessages: rawLineNo,
                updatedAt: Date.now(),
            });
            const replyIds = new Set([agentId]);
            for (const [subId, meta] of this.agentsWatchedForJsonl()) {
                if (subId === fileAgentId)
                    continue;
                const subPath = resolveJsonlFilePath(this.projectsDir, subId, this.resolveOpts(meta.title));
                if (subPath === filePath)
                    replyIds.add(subId);
            }
            const hasSubscriber = [...replyIds].some((id) => this.subscribed.has(id));
            const hasDrainer = [...replyIds].some((id) => this.draining.has(id));
            if (!hasSubscriber && !hasDrainer)
                return;
            if (this.historyReplyInFlight.has(agentId) ||
                this.historyReplyInFlight.has(fileAgentId) ||
                [...replyIds].some((id) => this.historyReplyInFlight.has(id))) {
                bridgePipelineLog({
                    dir: 'internal',
                    event: 'poll:agent:history:SKIP',
                    agentId,
                    detail: `inFlight file=${fileAgentId.slice(0, 8)}`,
                });
                return;
            }
            for (const replyId of replyIds) {
                if (!this.subscribed.has(replyId))
                    continue;
                this.emit('agent:history', {
                    agentId: replyId,
                    messages,
                    totalMessages: rawLineNo,
                    updatedAt: Date.now(),
                });
            }
        }
        catch {
            /* skip */
        }
    }
    scheduleRebuild() {
        if (this.debounceTimer)
            clearTimeout(this.debounceTimer);
        this.debounceTimer = setTimeout(() => void this.rebuild(), 500);
    }
    async rebuild(opts) {
        const broadcast = opts?.broadcast !== false;
        const repos = new Map();
        if (!existsSync(this.projectsDir)) {
            const empty = { repos: [], updatedAt: Date.now() };
            if (broadcast)
                this.emit('agents:index', empty);
            return empty;
        }
        for (const entry of readdirSync(this.projectsDir, { withFileTypes: true })) {
            if (!entry.isDirectory())
                continue;
            const repo = parseRepoFromProjectDir(entry.name);
            const transcriptsDir = join(this.projectsDir, entry.name, 'agent-transcripts');
            if (!existsSync(transcriptsDir))
                continue;
            const agents = [];
            collectAgents(transcriptsDir, agents);
            if (agents.length === 0)
                continue;
            agents.sort((a, b) => b.updatedAt - a.updatedAt);
            repos.set(repo.id, {
                id: repo.id,
                name: repo.name,
                path: join(this.projectsDir, entry.name),
                agents,
            });
        }
        const index = {
            repos: [...repos.values()].sort((a, b) => a.name.localeCompare(b.name)),
            updatedAt: Date.now(),
            listSource: 'jsonl',
        };
        if (broadcast)
            this.emit('agents:index', index);
        return index;
    }
    findProjectDirForAgent(agentId) {
        const filePath = findJsonlByAgentId(this.projectsDir, agentId);
        if (!filePath)
            return null;
        const idx = filePath.indexOf('/agent-transcripts/');
        if (idx < 0)
            return null;
        return filePath.slice(0, idx);
    }
    async loadHistory(agentId, opts) {
        const filePath = resolveJsonlFilePath(this.projectsDir, agentId, {
            ...this.resolveOpts(opts?.title),
            ...opts,
        });
        if (!filePath) {
            return { agentId, messages: [], totalMessages: 0 };
        }
        const messages = await this.loadMessagesForFile(filePath);
        const totalMessages = messages.length;
        const limit = opts?.limit;
        const offset = Math.max(0, opts?.offset ?? 0);
        let trimmed = messages;
        if (limit && limit > 0) {
            const end = offset > 0 ? -offset : undefined;
            const start = -(offset + limit);
            trimmed = messages.slice(start, end);
        }
        else if (offset > 0) {
            trimmed = messages.slice(0, -offset);
        }
        return { agentId, messages: trimmed, totalMessages };
    }
}
function shouldIgnoreWatchPath(p) {
    return p.includes('/subagents/') || p.includes('\\subagents\\');
}
function parseRepoFromProjectDir(dirName) {
    const prefix = 'Users-';
    const marker = '-Projects-';
    if (dirName.includes(marker)) {
        const name = dirName.split(marker).pop() ?? dirName;
        return { id: name, name: decodeSlug(name) };
    }
    if (dirName.startsWith(prefix)) {
        const rest = dirName.slice(prefix.length);
        return { id: rest, name: decodeSlug(rest) };
    }
    return { id: dirName, name: decodeSlug(dirName) };
}
function decodeSlug(s) {
    return s.replace(/-/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
}
function collectAgents(dir, out, depth = 0) {
    if (depth > 4)
        return;
    for (const entry of readdirSync(dir, { withFileTypes: true })) {
        const full = join(dir, entry.name);
        if (entry.isDirectory()) {
            collectAgents(full, out, depth + 1);
            continue;
        }
        if (!entry.name.endsWith('.jsonl'))
            continue;
        const agentId = basename(entry.name, '.jsonl');
        if (full.includes('/subagents/'))
            continue;
        try {
            const st = statSync(full);
            const preview = peekFirstUserMessage(full);
            out.push({
                id: agentId,
                title: preview?.slice(0, 80) || `Agent ${agentId.slice(0, 8)}`,
                updatedAt: st.mtimeMs,
                messageCount: countLines(full),
                preview,
            });
        }
        catch {
            /* skip */
        }
    }
}
function peekFirstUserMessage(filePath) {
    try {
        const buf = readFileHead(filePath, 8192);
        for (const line of buf.split('\n')) {
            if (!line.trim())
                continue;
            const row = JSON.parse(line);
            if (row.role !== 'user')
                continue;
            const text = row.message?.content?.find((c) => c.type === 'text')?.text ?? '';
            const cleaned = cleanUserText(text);
            if (cleaned)
                return cleaned.slice(0, 200);
        }
    }
    catch {
        /* ignore */
    }
    return undefined;
}
function readFileHead(path, max) {
    const fd = readFileSync(path);
    return fd.slice(0, max).toString('utf-8');
}
function countLines(path) {
    try {
        return readFileSync(path, 'utf-8').split('\n').filter((l) => l.trim()).length;
    }
    catch {
        return 0;
    }
}
function findJsonlByAgentId(projectsDir, agentId) {
    if (!existsSync(projectsDir))
        return null;
    for (const project of readdirSync(projectsDir, { withFileTypes: true })) {
        if (!project.isDirectory())
            continue;
        const base = join(projectsDir, project.name, 'agent-transcripts');
        const direct = join(base, agentId, `${agentId}.jsonl`);
        if (existsSync(direct))
            return direct;
        const found = walkForAgent(base, agentId);
        if (found)
            return found;
    }
    return null;
}
function walkForAgent(dir, agentId, depth = 0) {
    if (depth > 5 || !existsSync(dir))
        return null;
    for (const entry of readdirSync(dir, { withFileTypes: true })) {
        const full = join(dir, entry.name);
        if (entry.isDirectory()) {
            const r = walkForAgent(full, agentId, depth + 1);
            if (r)
                return r;
        }
        else if (entry.name === `${agentId}.jsonl`) {
            return full;
        }
    }
    return null;
}
function parseJsonlLine(line, lineNo) {
    if (!line.trim())
        return null;
    try {
        const row = JSON.parse(line);
        const parts = [];
        const rawParts = [];
        for (const c of row.message?.content ?? []) {
            if (c.type === 'text' && c.text) {
                rawParts.push(c.text);
                const t = cleanUserText(c.text);
                if (t)
                    parts.push(t);
            }
        }
        if (row.role === 'user') {
            const raw = rawParts.join('\n');
            const images = canonicalizeImagePaths(parseUserImagePaths(raw));
            const text = stripJsonlRedactionArtifacts(cleanUserText(parts.join('\n').trim()));
            const hasImages = images.length > 0;
            if ((!text || isNoiseChatText(text)) && !hasImages)
                return null;
            if (text && isNoiseChatText(text) && !hasImages)
                return null;
            return {
                role: 'user',
                text: text || (hasImages ? 'Изображение' : ''),
                images: hasImages ? images : undefined,
                ts: lineNo,
            };
        }
        if (row.role === 'assistant') {
            let planBlock;
            for (const c of row.message?.content ?? []) {
                if (c.type !== 'tool_use' || !c.name)
                    continue;
                if (c.name !== 'CreatePlan')
                    continue;
                const input = (c.input || {});
                const buildTitle = (input.name || '').trim() || 'Plan';
                const todos = (input.todos || [])
                    .map((t, i) => {
                    const text = (t.content || '').trim();
                    if (!text)
                        return null;
                    const statusRaw = (t.status || '').toLowerCase();
                    const status = statusRaw === 'completed' || statusRaw === 'done'
                        ? 'completed'
                        : statusRaw === 'cancelled' || statusRaw === 'canceled'
                            ? 'cancelled'
                            : statusRaw === 'in_progress' || statusRaw === 'in-progress'
                                ? 'in_progress'
                                : 'pending';
                    return {
                        id: (t.id || `plan-${lineNo}-${i}`).trim(),
                        text,
                        status: status,
                    };
                })
                    .filter(Boolean);
                if (!todos.length && buildTitle === 'Plan')
                    continue;
                const completedCount = todos.filter((t) => t.status === 'completed').length;
                planBlock = {
                    buildTitle,
                    todos,
                    completedCount,
                    totalCount: todos.length,
                };
            }
            const filtered = filterAssistantJsonlParts(parts);
            const text = filtered.join('\n').trim();
            if (planBlock && !text) {
                return { role: 'assistant', text: '', planBlock, ts: lineNo };
            }
            if (text)
                return { role: 'assistant', text, planBlock, ts: lineNo };
        }
    }
    catch {
        /* skip bad line */
    }
    return null;
}
async function parseJsonlFileFull(filePath) {
    const messages = [];
    const rl = createInterface({ input: createReadStream(filePath), crlfDelay: Infinity });
    let lineNo = 0;
    for await (const line of rl) {
        lineNo++;
        const m = parseJsonlLine(line, lineNo);
        if (m)
            messages.push(m);
    }
    return { lineNo, messages };
}
async function parseJsonlTail(filePath, afterLineNo) {
    const appended = [];
    const rl = createInterface({ input: createReadStream(filePath), crlfDelay: Infinity });
    let lineNo = 0;
    for await (const line of rl) {
        lineNo++;
        if (lineNo <= afterLineNo)
            continue;
        const m = parseJsonlLine(line, lineNo);
        if (m)
            appended.push(m);
    }
    return { lineNo, appended };
}
