import { createReadStream, statSync } from 'node:fs';
import { readdir } from 'node:fs/promises';
import { homedir } from 'node:os';
import { basename, join, resolve } from 'node:path';
import { createInterface } from 'node:readline';
import { logger } from './logging.js';
const MAX_TITLE_LEN = 60;
function isRecord(value) {
    return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
}
function stringifyUnknown(value) {
    if (typeof value === 'string')
        return value;
    if (value == null)
        return '';
    try {
        return JSON.stringify(value, null, 2);
    }
    catch {
        return String(value);
    }
}
function defaultClaudeRoot() {
    return join(homedir(), '.claude', 'projects');
}
function defaultCodexRoot() {
    return join(homedir(), '.codex', 'sessions');
}
async function safeReaddir(dir) {
    try {
        return await readdir(dir);
    }
    catch {
        return [];
    }
}
async function listJsonlFiles(root, recursive) {
    const out = [];
    async function walk(dir, depth) {
        const entries = await safeReaddir(dir);
        for (const entry of entries) {
            if (entry.startsWith('.'))
                continue;
            const full = join(dir, entry);
            let stat;
            try {
                stat = statSync(full);
            }
            catch {
                continue;
            }
            if (stat.isDirectory()) {
                if (recursive && depth < 8)
                    await walk(full, depth + 1);
            }
            else if (stat.isFile() && entry.endsWith('.jsonl')) {
                out.push(full);
            }
        }
    }
    await walk(root, 0);
    return out;
}
async function readLines(file, onLine) {
    const stream = createReadStream(file, { encoding: 'utf-8' });
    const rl = createInterface({ input: stream, crlfDelay: Infinity });
    try {
        for await (const line of rl) {
            if (!line)
                continue;
            const stop = onLine(line);
            if (stop === true)
                break;
        }
    }
    finally {
        rl.close();
        stream.destroy();
    }
}
function parseLineSafe(line) {
    try {
        const parsed = JSON.parse(line);
        return isRecord(parsed) ? parsed : null;
    }
    catch {
        return null;
    }
}
function deriveTitle(text, fallback) {
    const trimmed = text.trim().replace(/\s+/g, ' ');
    if (!trimmed)
        return fallback;
    return trimmed.length > MAX_TITLE_LEN ? `${trimmed.slice(0, MAX_TITLE_LEN - 1)}…` : trimmed;
}
function flattenClaudeContent(content) {
    if (typeof content === 'string') {
        return { text: content, hasText: content.trim().length > 0, hasToolUse: false, hasToolResult: false, rawContent: content };
    }
    if (!Array.isArray(content)) {
        return { text: '', hasText: false, hasToolUse: false, hasToolResult: false, rawContent: content };
    }
    let hasText = false;
    let hasToolUse = false;
    let hasToolResult = false;
    const parts = [];
    for (const block of content) {
        if (!isRecord(block))
            continue;
        if (typeof block.text === 'string') {
            if (block.text.trim())
                hasText = true;
            parts.push(block.text);
            continue;
        }
        if (block.type === 'tool_use') {
            hasToolUse = true;
            parts.push(`Tool call: ${stringifyUnknown(block.name)}\n${stringifyUnknown(block.input)}`);
            continue;
        }
        if (block.type === 'tool_result') {
            hasToolResult = true;
            parts.push(`Tool result:\n${stringifyUnknown(block.content)}`);
            continue;
        }
    }
    return { text: parts.filter(Boolean).join('\n'), hasText, hasToolUse, hasToolResult, rawContent: content };
}
async function discoverClaude(root) {
    const files = await listJsonlFiles(root, true);
    const out = [];
    for (const filePath of files) {
        const fileId = basename(filePath, '.jsonl');
        let cwd = null;
        let sessionId = null;
        let isSidechain = filePath.includes('/subagents/') || fileId.startsWith('agent-');
        await readLines(filePath, (line) => {
            const obj = parseLineSafe(line);
            if (!obj)
                return false;
            if (typeof obj.cwd === 'string' && !cwd)
                cwd = obj.cwd;
            if (typeof obj.sessionId === 'string' && !sessionId)
                sessionId = obj.sessionId;
            if (obj.isSidechain === true)
                isSidechain = true;
            return Boolean(cwd && sessionId);
        });
        if (!cwd)
            continue;
        const id = isSidechain && sessionId ? `${sessionId}:${fileId}` : sessionId ?? fileId;
        out.push({ source: 'claude', filePath, providerSessionId: id, cwd: resolve(cwd) });
    }
    return out;
}
async function discoverCodex(root) {
    const files = await listJsonlFiles(root, true);
    const out = [];
    for (const filePath of files) {
        let cwd = null;
        let sessionId = null;
        await readLines(filePath, (line) => {
            const obj = parseLineSafe(line);
            if (!obj)
                return false;
            if (obj.type === 'session_meta' && isRecord(obj.payload)) {
                if (typeof obj.payload.cwd === 'string')
                    cwd = obj.payload.cwd;
                if (typeof obj.payload.id === 'string')
                    sessionId = obj.payload.id;
                return Boolean(cwd && sessionId);
            }
            return false;
        });
        if (!cwd || !sessionId) {
            logger.warn('sessionImport.codex_no_meta', { filePath });
            continue;
        }
        out.push({ source: 'codex', filePath, providerSessionId: sessionId, cwd: resolve(cwd) });
    }
    return out;
}
function groupByCwd(items) {
    const buckets = new Map();
    for (const item of items) {
        const key = resolve(item.cwd);
        const list = buckets.get(key) ?? [];
        list.push(item);
        buckets.set(key, list);
    }
    const groups = [];
    for (const [cwd, list] of buckets.entries()) {
        const sources = new Set(list.map((d) => d.source));
        const source = sources.size > 1 ? 'mixed' : list[0].source;
        groups.push({
            cwd,
            source,
            sessionCount: list.length,
            files: list.map((d) => ({
                source: d.source,
                path: d.filePath,
                providerSessionId: d.providerSessionId
            }))
        });
    }
    groups.sort((a, b) => a.cwd.localeCompare(b.cwd));
    return groups;
}
export async function scanExternalSessions(roots) {
    const claudeRoot = roots?.claude ?? defaultClaudeRoot();
    const codexRoot = roots?.codex ?? defaultCodexRoot();
    const [claude, codex] = await Promise.all([discoverClaude(claudeRoot), discoverCodex(codexRoot)]);
    const discovered = [...claude, ...codex];
    return { groups: groupByCwd(discovered), discovered };
}
function fileMtimeIso(filePath) {
    try {
        return statSync(filePath).mtime.toISOString();
    }
    catch {
        return new Date().toISOString();
    }
}
function safeTimestamp(value, fallback) {
    if (typeof value === 'string') {
        const t = Date.parse(value);
        if (Number.isFinite(t))
            return new Date(t).toISOString();
    }
    if (typeof value === 'number' && Number.isFinite(value)) {
        const ms = value > 1e12 ? value : value * 1000;
        return new Date(ms).toISOString();
    }
    return fallback;
}
async function parseClaudeFile(filePath, providerSessionId) {
    const fallbackTs = fileMtimeIso(filePath);
    let cwd = null;
    let model = null;
    let permissionMode = null;
    let firstUserText = '';
    const messages = [];
    await readLines(filePath, (line) => {
        const obj = parseLineSafe(line);
        if (!obj)
            return false;
        if (typeof obj.cwd === 'string' && !cwd)
            cwd = obj.cwd;
        if (obj.type === 'permission-mode' && typeof obj.permissionMode === 'string') {
            permissionMode = obj.permissionMode;
            return false;
        }
        if (obj.type === 'file-history-snapshot' || obj.type === 'summary')
            return false;
        if (obj.type !== 'user' && obj.type !== 'assistant')
            return false;
        const inner = isRecord(obj.message) ? obj.message : null;
        if (!inner)
            return false;
        const role = inner.role === 'assistant' ? 'assistant' : 'user';
        if (typeof inner.model === 'string' && !model)
            model = inner.model;
        const ts = safeTimestamp(obj.timestamp, fallbackTs);
        const flat = flattenClaudeContent(inner.content);
        if (!flat.text && !flat.hasToolUse && !flat.hasToolResult)
            return false;
        let outRole = role;
        let eventType = `claude.import.${role}`;
        if (!flat.hasText && (flat.hasToolUse || flat.hasToolResult)) {
            outRole = 'tool';
            eventType = 'claude.import.tool';
        }
        const metadata = {
            source: 'claude',
            rawContent: flat.rawContent
        };
        if (typeof obj.agentId === 'string')
            metadata.agentId = obj.agentId;
        if (typeof obj.isSidechain === 'boolean')
            metadata.isSidechain = obj.isSidechain;
        if (typeof obj.uuid === 'string')
            metadata.uuid = obj.uuid;
        if (typeof obj.parentUuid === 'string' || obj.parentUuid === null)
            metadata.parentUuid = obj.parentUuid;
        if (typeof obj.gitBranch === 'string')
            metadata.gitBranch = obj.gitBranch;
        if (typeof obj.version === 'string')
            metadata.cliVersion = obj.version;
        messages.push({ role: outRole, content: flat.text, eventType, createdAt: ts, metadata });
        if (role === 'user' && !firstUserText && flat.hasText) {
            firstUserText = flat.text;
        }
        return false;
    });
    if (!cwd)
        return null;
    if (messages.length === 0)
        return null;
    messages.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
    const startedAt = messages[0].createdAt;
    const endedAt = messages[messages.length - 1].createdAt;
    const derivedTitle = deriveTitle(firstUserText, basename(cwd) || 'Claude import');
    return {
        source: 'claude',
        providerSessionId,
        cwd: resolve(cwd),
        startedAt,
        endedAt,
        model,
        permissionMode,
        derivedTitle,
        messages
    };
}
function flattenCodexContent(content) {
    if (typeof content === 'string')
        return { text: content, raw: content };
    if (!Array.isArray(content))
        return { text: '', raw: content };
    const parts = [];
    for (const block of content) {
        if (!isRecord(block))
            continue;
        if (typeof block.text === 'string')
            parts.push(block.text);
        else if (block.type === 'input_text' && typeof block.text === 'string')
            parts.push(block.text);
        else if (block.type === 'output_text' && typeof block.text === 'string')
            parts.push(block.text);
    }
    return { text: parts.filter(Boolean).join('\n'), raw: content };
}
function isSyntheticCodexUserText(text) {
    const t = text.trim();
    return t.startsWith('<environment_context>') || t.startsWith('<user_instructions>') || t.startsWith('<permissions');
}
async function parseCodexFile(filePath, providerSessionId) {
    const fallbackTs = fileMtimeIso(filePath);
    let cwd = null;
    let model = null;
    let firstUserText = '';
    const messages = [];
    await readLines(filePath, (line) => {
        const obj = parseLineSafe(line);
        if (!obj)
            return false;
        const outerTs = safeTimestamp(obj.timestamp, fallbackTs);
        if (obj.type === 'session_meta' && isRecord(obj.payload)) {
            if (typeof obj.payload.cwd === 'string' && !cwd)
                cwd = obj.payload.cwd;
            if (typeof obj.payload.model === 'string' && !model)
                model = obj.payload.model;
            return false;
        }
        if (obj.type === 'turn_context' && isRecord(obj.payload)) {
            if (typeof obj.payload.model === 'string')
                model = obj.payload.model;
            return false;
        }
        if (obj.type === 'response_item' && isRecord(obj.payload)) {
            const payload = obj.payload;
            const itemType = typeof payload.type === 'string' ? payload.type : '';
            if (itemType === 'message') {
                const rawRole = typeof payload.role === 'string' ? payload.role : 'user';
                const role = rawRole === 'assistant' ? 'assistant' : rawRole === 'developer' || rawRole === 'system' ? 'system' : 'user';
                const flat = flattenCodexContent(payload.content);
                if (!flat.text.trim())
                    return false;
                const synthetic = role === 'user' && isSyntheticCodexUserText(flat.text);
                const metadata = {
                    source: 'codex',
                    rawContent: flat.raw
                };
                if (synthetic)
                    metadata.synthetic = true;
                messages.push({
                    role,
                    content: flat.text,
                    eventType: `codex.import.message.${role}`,
                    createdAt: outerTs,
                    metadata
                });
                if (role === 'user' && !firstUserText && !synthetic)
                    firstUserText = flat.text;
                return false;
            }
            if (itemType === 'function_call') {
                const name = stringifyUnknown(payload.name);
                const args = stringifyUnknown(payload.arguments);
                const callId = typeof payload.call_id === 'string' ? payload.call_id : undefined;
                messages.push({
                    role: 'tool',
                    content: `Tool call: ${name}\n${args}`,
                    eventType: 'codex.import.function_call',
                    createdAt: outerTs,
                    metadata: { source: 'codex', toolCallId: callId, rawContent: payload }
                });
                return false;
            }
            if (itemType === 'function_call_output') {
                const callId = typeof payload.call_id === 'string' ? payload.call_id : undefined;
                const output = stringifyUnknown(payload.output);
                messages.push({
                    role: 'tool',
                    content: `Tool result:\n${output}`,
                    eventType: 'codex.import.function_call_output',
                    createdAt: outerTs,
                    metadata: { source: 'codex', toolCallId: callId, rawContent: payload }
                });
                return false;
            }
            if (itemType === 'reasoning') {
                const summary = Array.isArray(payload.summary) ? payload.summary : [];
                const text = summary
                    .map((s) => (isRecord(s) && typeof s.text === 'string' ? s.text : ''))
                    .filter(Boolean)
                    .join('\n');
                if (!text.trim())
                    return false;
                messages.push({
                    role: 'system',
                    content: text,
                    eventType: 'codex.import.reasoning',
                    createdAt: outerTs,
                    metadata: { source: 'codex', rawContent: payload }
                });
                return false;
            }
            return false;
        }
        if (obj.type === 'event_msg' && isRecord(obj.payload)) {
            const payload = obj.payload;
            if (payload.type === 'error') {
                const msg = typeof payload.message === 'string' ? payload.message : stringifyUnknown(payload);
                messages.push({
                    role: 'error',
                    content: msg,
                    eventType: 'codex.import.error',
                    createdAt: outerTs,
                    metadata: { source: 'codex', rawContent: payload }
                });
            }
            return false;
        }
        return false;
    });
    if (!cwd)
        return null;
    if (messages.length === 0)
        return null;
    messages.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
    const startedAt = messages[0].createdAt;
    const endedAt = messages[messages.length - 1].createdAt;
    const derivedTitle = deriveTitle(firstUserText, basename(cwd) || 'Codex import');
    return {
        source: 'codex',
        providerSessionId,
        cwd: resolve(cwd),
        startedAt,
        endedAt,
        model,
        permissionMode: null,
        derivedTitle,
        messages
    };
}
async function parseFile(item) {
    if (item.source === 'claude')
        return parseClaudeFile(item.filePath, item.providerSessionId);
    return parseCodexFile(item.filePath, item.providerSessionId);
}
export async function importExternalSessions(deps, filter = {}) {
    const { db, git } = deps;
    let scopedPath = null;
    if (filter.projectId) {
        const project = db.getProject(filter.projectId);
        if (!project) {
            throw new Error(`project not found: ${filter.projectId}`);
        }
        scopedPath = resolve(project.path);
    }
    else if (filter.cwd) {
        scopedPath = resolve(filter.cwd);
    }
    const { discovered } = await scanExternalSessions(deps.roots);
    const scoped = scopedPath ? discovered.filter((d) => d.cwd === scopedPath) : discovered;
    const filtered = scoped.filter((d) => !db.isProjectImportBlocked(d.cwd));
    const byCwd = new Map();
    for (const item of filtered) {
        const list = byCwd.get(item.cwd) ?? [];
        list.push(item);
        byCwd.set(item.cwd, list);
    }
    const result = {
        scanned: filtered.length,
        imported: 0,
        skipped: 0,
        failed: 0,
        createdProjects: [],
        attachedToProjects: [],
        groups: []
    };
    for (const [cwd, items] of byCwd.entries()) {
        let project = db.getProjectByPath(cwd);
        if (!project) {
            let isGit = false;
            try {
                isGit = await git.isRepo(cwd);
            }
            catch {
                isGit = false;
            }
            project = db.addProject({ path: cwd, name: basename(cwd) || cwd, isGitRepo: isGit });
            result.createdProjects.push(project.id);
        }
        else if (!result.attachedToProjects.includes(project.id)) {
            result.attachedToProjects.push(project.id);
        }
        let groupMessageCount = 0;
        for (const item of items) {
            try {
                const existing = db.findAgentSessionByProviderId(item.source, item.providerSessionId);
                if (existing) {
                    result.skipped += 1;
                    continue;
                }
                const parsed = await parseFile(item);
                if (!parsed || parsed.messages.length === 0) {
                    result.failed += 1;
                    continue;
                }
                db.transaction(() => {
                    const session = db.importAgentSession({
                        projectId: project.id,
                        provider: parsed.source,
                        title: parsed.derivedTitle,
                        status: 'exited',
                        model: parsed.model,
                        permissionMode: parsed.permissionMode,
                        providerSessionId: parsed.providerSessionId,
                        createdAt: parsed.startedAt,
                        updatedAt: parsed.endedAt
                    });
                    for (const message of parsed.messages) {
                        db.addImportedAgentMessage({
                            sessionId: session.id,
                            role: message.role,
                            content: message.content,
                            eventType: message.eventType,
                            createdAt: message.createdAt,
                            metadata: message.metadata
                        });
                    }
                });
                groupMessageCount += parsed.messages.length;
                result.imported += 1;
            }
            catch (err) {
                logger.warn('sessionImport.file_failed', {
                    filePath: item.filePath,
                    error: err instanceof Error ? err.message : String(err)
                });
                result.failed += 1;
            }
        }
        result.groups.push({ cwd, sessionCount: items.length, messageCount: groupMessageCount });
    }
    return result;
}
export async function scanExternalSessionsFiltered(deps, filter = {}) {
    const { groups } = await scanExternalSessions(deps.roots);
    let scopedPath = null;
    if (filter.projectId) {
        const project = deps.db.getProject(filter.projectId);
        if (!project)
            return [];
        scopedPath = resolve(project.path);
    }
    else if (filter.cwd) {
        scopedPath = resolve(filter.cwd);
    }
    const visibleGroups = groups.filter((g) => !deps.db.isProjectImportBlocked(g.cwd));
    if (!scopedPath)
        return visibleGroups;
    return visibleGroups.filter((g) => g.cwd === scopedPath);
}
//# sourceMappingURL=sessionImport.js.map