import { readFileSync, writeFileSync, mkdirSync, existsSync, renameSync, readdirSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { randomUUID } from 'node:crypto';
import { config } from '../config.js';
import { logger } from '../utils/logger.js';
import { mergePendingResponseState } from '../core/pending-response.js';
import { deleteFrozenCards } from './frozen-card-store.js';
let sessions = new Map();
let loaded = false;
let currentAppId;
/**
 * Initialise session store for a specific bot (multi-daemon mode).
 * When appId is set, sessions are stored in `sessions-{appId}.json`.
 * When unset, uses the legacy `sessions.json`.
 */
export function init(appId) {
    currentAppId = appId;
    loaded = false;
    sessions = new Map();
}
function getFilePath() {
    const fileName = currentAppId ? `sessions-${currentAppId}.json` : 'sessions.json';
    return join(config.session.dataDir, fileName);
}
function ensureDir() {
    const dir = dirname(getFilePath());
    if (!existsSync(dir)) {
        mkdirSync(dir, { recursive: true });
    }
}
// Sessions persisted before 2026-04-29 lack `cliId`; consumers must fall back to 'unknown' at the render boundary.
function load() {
    if (loaded)
        return;
    ensureDir();
    const fp = getFilePath();
    if (existsSync(fp)) {
        try {
            const data = JSON.parse(readFileSync(fp, 'utf-8'));
            sessions = new Map(Object.entries(data));
            logger.info(`Loaded ${sessions.size} sessions from ${fp}`);
        }
        catch (err) {
            logger.error(`Failed to load sessions: ${err}`);
            sessions = new Map();
        }
    }
    else if (currentAppId) {
        // Per-bot file doesn't exist — migrate matching sessions from legacy sessions.json
        const legacyFp = join(config.session.dataDir, 'sessions.json');
        if (existsSync(legacyFp)) {
            try {
                const data = JSON.parse(readFileSync(legacyFp, 'utf-8'));
                sessions = new Map();
                for (const [k, v] of Object.entries(data)) {
                    if (v.larkAppId === currentAppId) {
                        sessions.set(k, v);
                    }
                }
                if (sessions.size > 0) {
                    save();
                    logger.info(`Migrated ${sessions.size} sessions from sessions.json to ${fp}`);
                }
            }
            catch (err) {
                logger.error(`Failed to migrate sessions from legacy file: ${err}`);
                sessions = new Map();
            }
        }
    }
    loaded = true;
}
function readExistingSessionsFromDisk(fp) {
    if (!existsSync(fp))
        return { raw: '', parsed: {} };
    try {
        const raw = readFileSync(fp, 'utf-8');
        return { raw, parsed: JSON.parse(raw) };
    }
    catch {
        return { raw: '', parsed: {} };
    }
}
function save() {
    ensureDir();
    const fp = getFilePath();
    const { raw: existingRaw, parsed: existing } = readExistingSessionsFromDisk(fp);
    const obj = {};
    for (const [k, v] of sessions) {
        const merged = mergePendingResponseState(v, existing[k]);
        sessions.set(k, merged);
        obj[k] = merged;
    }
    const json = JSON.stringify(obj, null, 2);
    // The daemon fires several updateSession()/save() calls per inbound message
    // (activity bump, pid, stream-card state, …) and many leave the serialized
    // file byte-identical. Skipping the temp-file write + rename in that case
    // elides the bulk of the redundant disk I/O — and writing identical bytes is
    // a guaranteed no-op, so this can't drop state or race a concurrent writer
    // (we compare against what's actually on disk right now).
    if (json === existingRaw)
        return;
    const tmpFp = `${fp}.${process.pid}.${randomUUID()}.tmp`;
    writeFileSync(tmpFp, json, 'utf-8');
    renameSync(tmpFp, fp);
}
export function createSession(chatId, rootMessageId, title, chatType) {
    load();
    const session = {
        sessionId: randomUUID(),
        chatId,
        chatType,
        rootMessageId,
        title,
        status: 'active',
        createdAt: new Date().toISOString(),
    };
    sessions.set(session.sessionId, session);
    save();
    logger.info(`Created session ${session.sessionId} (thread: ${rootMessageId})`);
    return session;
}
export function getSession(sessionId) {
    load();
    return sessions.get(sessionId) ?? findInOtherFiles(sessionId);
}
/**
 * Search all session files for a session not found in the current file.
 *
 * Sessions are partitioned per-bot (sessions-<larkAppId>.json), but agent-
 * facing CLI subcommands (`botmux send`, etc.) may be invoked in contexts
 * where LARK_APP_ID isn't set, so they can't pick the right file directly.
 * Scanning all files is safe — these callers only read sessions.
 */
function findInOtherFiles(sessionId) {
    const dataDir = config.session.dataDir;
    const currentFp = getFilePath();
    try {
        for (const file of readdirSync(dataDir)) {
            if (!file.startsWith('sessions') || !file.endsWith('.json'))
                continue;
            const fp = join(dataDir, file);
            if (fp === currentFp)
                continue;
            try {
                const data = JSON.parse(readFileSync(fp, 'utf-8'));
                if (data[sessionId])
                    return data[sessionId];
            }
            catch {
                continue;
            }
        }
    }
    catch { /* ignore */ }
    return undefined;
}
export function closeSession(sessionId) {
    load();
    const session = sessions.get(sessionId);
    if (session) {
        session.status = 'closed';
        session.closedAt = new Date().toISOString();
        save();
        deleteFrozenCards(sessionId);
        logger.info(`Closed session ${sessionId}`);
    }
}
export function updateSessionPid(sessionId, pid) {
    load();
    const session = sessions.get(sessionId);
    if (session) {
        session.pid = pid ?? undefined;
        save();
    }
}
export function updateSession(session) {
    load();
    sessions.set(session.sessionId, session);
    save();
}
export function listSessions() {
    load();
    return [...sessions.values()];
}
/**
 * Cross-file lookup: find every active session attached to a thread, across
 * all bots. Used when a not-yet-initialized bot is mentioned in a thread that
 * another bot has already pinned to a working directory — the new bot inherits
 * the pinned dir instead of re-prompting the user for repo selection.
 *
 * Reads other bots' session files directly (best-effort) instead of relying on
 * any in-memory state, since each daemon process only owns its own bot.
 */
export function findActiveSessionsByRoot(rootMessageId) {
    return findActiveSessionsMatching(s => s.rootMessageId === rootMessageId);
}
/**
 * Cross-file lookup: find every active chat-scope session for a chat, across
 * all bots. Mirror of findActiveSessionsByRoot for chat-scope (普通群整群一会话):
 * lets a not-yet-initialised bot inherit the workingDir from a peer bot that
 * already has a chat-scope session in the same chat, so a `botmux send
 * --mention <other-bot>` in 普通群 can spawn the second bot without bouncing
 * through the repo-select card.
 *
 * Only returns scope='chat' sessions — thread-scope sessions in the same chat
 * are routed by rootMessageId and not eligible for chat-scope inheritance.
 */
export function findActiveChatScopeSessionsByChat(chatId) {
    return findActiveSessionsMatching(s => s.chatId === chatId && s.scope === 'chat');
}
/**
 * Count active sessions across every bot's on-disk session file. A pure disk
 * read (no in-memory state) so it's correct at daemon startup regardless of
 * which bot owns this process — used by the restart-report DM after a restart.
 */
export function countActiveSessionsOnDisk(dataDir = config.session.dataDir) {
    let n = 0;
    try {
        for (const file of readdirSync(dataDir)) {
            if (!file.startsWith('sessions') || !file.endsWith('.json'))
                continue;
            try {
                const data = JSON.parse(readFileSync(join(dataDir, file), 'utf-8'));
                for (const s of Object.values(data))
                    if (s?.status === 'active')
                        n++;
            }
            catch {
                continue;
            }
        }
    }
    catch { /* missing dir → 0 */ }
    return n;
}
/**
 * Collect every CLI session identity botmux has ever recorded — across ALL bot
 * store files, ANY status (active or closed). Returns both each session's
 * botmux `sessionId` (which, for claude-family, IS the on-disk jsonl filename
 * since botmux spawns with `--session-id <id>`) and its `cliSessionId` (the
 * CLI-native id after any resume/rotation, e.g. a codex/traex rollout id).
 *
 * Used by `/adopt`'s resume-import discovery to hide sessions botmux already
 * manages — live OR closed — so the picker surfaces only genuinely external
 * sessions (a CLI the user ran standalone). Closed botmux sessions remain
 * resumable via their own session-closed cards.
 */
export function collectBotmuxSessionIdentities(dataDir = config.session.dataDir) {
    const ids = new Set();
    const add = (s) => {
        if (!s)
            return;
        if (s.sessionId)
            ids.add(s.sessionId);
        if (s.cliSessionId)
            ids.add(s.cliSessionId);
    };
    // In-memory first (freshest — covers ids not yet flushed to disk).
    load();
    for (const s of sessions.values())
        add(s);
    // Then every bot's persisted store file (other daemons own their own files).
    try {
        for (const file of readdirSync(dataDir)) {
            if (!file.startsWith('sessions') || !file.endsWith('.json'))
                continue;
            try {
                const data = JSON.parse(readFileSync(join(dataDir, file), 'utf-8'));
                for (const s of Object.values(data))
                    add(s);
            }
            catch {
                continue;
            }
        }
    }
    catch { /* missing dir → in-memory only */ }
    return ids;
}
function findActiveSessionsMatching(predicate) {
    load();
    const matches = [];
    for (const s of sessions.values()) {
        if (predicate(s) && s.status === 'active')
            matches.push(s);
    }
    const dataDir = config.session.dataDir;
    const currentFp = getFilePath();
    try {
        for (const file of readdirSync(dataDir)) {
            if (!file.startsWith('sessions') || !file.endsWith('.json'))
                continue;
            const fp = join(dataDir, file);
            if (fp === currentFp)
                continue;
            try {
                const data = JSON.parse(readFileSync(fp, 'utf-8'));
                for (const s of Object.values(data)) {
                    if (predicate(s) && s.status === 'active')
                        matches.push(s);
                }
            }
            catch {
                continue;
            }
        }
    }
    catch { /* ignore */ }
    return matches;
}
//# sourceMappingURL=session-store.js.map