import { existsSync, mkdirSync, rmSync, statSync } from "node:fs";
import { dirname, isAbsolute, join, resolve as resolvePath } from "node:path";
import { CACHE_FORMAT_VERSION, DEFAULT_CACHE_MAX_MB, DEFAULT_CACHE_TTL_DAYS, buildPortableCacheRow, createEmptyCacheCounts, parseCacheJson, } from "@steipete/summarize-core/runtime";
import { buildAttachmentContentHash, buildExtractCacheKeyValue, buildLanguageKey, buildLengthKey, buildPromptContentHash, buildPromptHash, buildSlidesCacheKeyValue, buildSummaryCacheKeyValue, buildTranscriptCacheKeyValue, hashJson, hashString, hashBytes, normalizeContentForHash, extractTaggedBlock, } from "./cache-keys.js";
export { buildAttachmentContentHash, buildExtractCacheKeyValue, buildLanguageKey, buildLengthKey, buildPromptContentHash, buildPromptHash, buildSlidesCacheKeyValue, buildSummaryCacheKeyValue, buildTranscriptCacheKeyValue, hashJson, hashString, hashBytes, normalizeContentForHash, extractTaggedBlock, } from "./cache-keys.js";
export { CACHE_FORMAT_VERSION, DEFAULT_CACHE_MAX_MB, DEFAULT_CACHE_TTL_DAYS, } from "@steipete/summarize-core/runtime";
import { cleanupSlidesPayload, collectSlidesPayloadArtifactPaths } from "./cache-slides-cleanup.js";
const TRANSCRIPT_SOURCES = [
    "youtubei",
    "captionTracks",
    "yt-dlp",
    "podcastTranscript",
    "whisper",
    "apify",
    "html",
    "unavailable",
    "unknown",
];
function normalizeTranscriptSource(value) {
    if (typeof value !== "string")
        return null;
    return TRANSCRIPT_SOURCES.includes(value)
        ? value
        : null;
}
const isBun = typeof globalThis.Bun !== "undefined";
let warningFilterInstalled = false;
const installSqliteWarningFilter = () => {
    if (warningFilterInstalled)
        return;
    warningFilterInstalled = true;
    const original = process.emitWarning.bind(process);
    process.emitWarning = ((warning, ...args) => {
        const message = typeof warning === "string"
            ? warning
            : warning && typeof warning.message === "string"
                ? String(warning.message)
                : "";
        const type = typeof args[0] === "string" ? args[0] : args[0]?.type;
        const name = warning?.name;
        const normalizedType = typeof type === "string" ? type : typeof name === "string" ? name : "";
        if (normalizedType === "ExperimentalWarning" && message.toLowerCase().includes("sqlite")) {
            return;
        }
        return original(warning, ...args);
    });
};
async function openSqlite(path) {
    if (isBun) {
        const mod = (await import("bun:sqlite"));
        return new mod.Database(path);
    }
    installSqliteWarningFilter();
    const mod = (await import("node:sqlite"));
    return new mod.DatabaseSync(path);
}
function ensureDir(path) {
    mkdirSync(path, { recursive: true });
}
function resolveHomeDir(env) {
    const home = env.HOME?.trim() || env.USERPROFILE?.trim();
    return home || null;
}
export function resolveCachePath({ env, cachePath, }) {
    const home = resolveHomeDir(env);
    const raw = cachePath?.trim();
    if (raw && raw.length > 0) {
        if (raw.startsWith("~")) {
            if (!home)
                return null;
            const expanded = raw === "~" ? home : join(home, raw.slice(2));
            return resolvePath(expanded);
        }
        return isAbsolute(raw) ? raw : home ? resolvePath(join(home, raw)) : null;
    }
    if (!home)
        return null;
    return join(home, ".summarize", "cache.sqlite");
}
export async function createCacheStore({ path, maxBytes, transcriptNamespace, }) {
    ensureDir(dirname(path));
    const db = await openSqlite(path);
    db.exec("PRAGMA busy_timeout=5000");
    db.exec("PRAGMA journal_mode=WAL");
    db.exec("PRAGMA synchronous=NORMAL");
    db.exec("PRAGMA auto_vacuum=INCREMENTAL");
    db.exec(`
    CREATE TABLE IF NOT EXISTS cache_entries (
      kind TEXT NOT NULL,
      key TEXT NOT NULL,
      value TEXT NOT NULL,
      size_bytes INTEGER NOT NULL,
      created_at INTEGER NOT NULL,
      last_accessed_at INTEGER NOT NULL,
      expires_at INTEGER,
      PRIMARY KEY (kind, key)
    )
  `);
    db.exec("CREATE INDEX IF NOT EXISTS idx_cache_accessed ON cache_entries(last_accessed_at)");
    db.exec("CREATE INDEX IF NOT EXISTS idx_cache_expires ON cache_entries(expires_at)");
    const stmtGet = db.prepare("SELECT value, expires_at, size_bytes, created_at FROM cache_entries WHERE kind = ? AND key = ?");
    const stmtTouch = db.prepare("UPDATE cache_entries SET last_accessed_at = ? WHERE kind = ? AND key = ?");
    const stmtDelete = db.prepare("DELETE FROM cache_entries WHERE kind = ? AND key = ?");
    const stmtSelectExpired = db.prepare("SELECT kind, key, value, created_at FROM cache_entries WHERE expires_at IS NOT NULL AND expires_at <= ?");
    const stmtUpsert = db.prepare(`
    INSERT INTO cache_entries (
      kind, key, value, size_bytes, created_at, last_accessed_at, expires_at
    ) VALUES (?, ?, ?, ?, ?, ?, ?)
    ON CONFLICT(kind, key) DO UPDATE SET
      value = excluded.value,
      size_bytes = excluded.size_bytes,
      created_at = excluded.created_at,
      last_accessed_at = excluded.last_accessed_at,
      expires_at = excluded.expires_at
  `);
    const stmtTotalSize = db.prepare("SELECT COALESCE(SUM(size_bytes), 0) AS total FROM cache_entries");
    const stmtOldest = db.prepare("SELECT kind, key, value, size_bytes, created_at FROM cache_entries ORDER BY last_accessed_at ASC LIMIT ?");
    const stmtSlides = db.prepare("SELECT kind, key, value FROM cache_entries WHERE kind = 'slides'");
    const stmtClear = db.prepare("DELETE FROM cache_entries");
    const buildReferencedSlideArtifacts = (excludingKey) => {
        const rows = stmtSlides.all();
        const referenced = new Set();
        for (const row of rows) {
            if (row.key === excludingKey)
                continue;
            for (const filePath of collectSlidesPayloadArtifactPaths(row.value)) {
                referenced.add(filePath);
            }
        }
        return referenced;
    };
    const deleteEntry = (kind, key, value, createdAt) => {
        if (kind === "slides") {
            const referenced = buildReferencedSlideArtifacts(key);
            cleanupSlidesPayload(value, { preservePaths: referenced, preserveNewerThanMs: createdAt });
        }
        stmtDelete.run(kind, key);
    };
    const sweepExpired = (now) => {
        const rows = stmtSelectExpired.all(now);
        for (const row of rows) {
            deleteEntry(row.kind, row.key, row.value, row.created_at);
        }
    };
    const enforceSize = () => {
        if (!Number.isFinite(maxBytes) || maxBytes <= 0)
            return;
        const row = stmtTotalSize.get();
        let total = typeof row?.total === "number" ? row.total : 0;
        if (total <= maxBytes)
            return;
        const batchSize = 50;
        while (total > maxBytes) {
            const rows = stmtOldest.all(batchSize);
            if (rows.length === 0)
                break;
            for (const row of rows) {
                if (total <= maxBytes)
                    break;
                deleteEntry(row.kind, row.key, row.value, row.created_at);
                total -= row.size_bytes ?? 0;
            }
            if (total <= maxBytes)
                break;
        }
        db.exec("PRAGMA incremental_vacuum");
    };
    const readEntry = (kind, key, now) => {
        const row = stmtGet.get(kind, key);
        if (!row)
            return null;
        const expiresAt = row.expires_at;
        if (typeof expiresAt === "number" && expiresAt <= now) {
            deleteEntry(kind, key, row.value, row.created_at);
            return { ...row, expires_at: expiresAt };
        }
        stmtTouch.run(now, kind, key);
        return row;
    };
    const getText = (kind, key) => {
        const now = Date.now();
        const row = readEntry(kind, key, now);
        if (!row)
            return null;
        const expiresAt = row.expires_at;
        if (typeof expiresAt === "number" && expiresAt <= now)
            return null;
        return row.value;
    };
    const getJson = (kind, key) => {
        return parseCacheJson(getText(kind, key));
    };
    const setText = (kind, key, value, ttlMs) => {
        const now = Date.now();
        sweepExpired(now);
        const row = buildPortableCacheRow({ kind, key, value, ttlMs, now });
        stmtUpsert.run(row.kind, row.key, row.value, row.sizeBytes, row.createdAt, row.lastAccessedAt, row.expiresAt);
        enforceSize();
    };
    const setJson = (kind, key, value, ttlMs) => {
        setText(kind, key, JSON.stringify(value), ttlMs);
    };
    const clear = () => {
        const rows = stmtSlides.all();
        for (const row of rows) {
            cleanupSlidesPayload(row.value);
        }
        stmtClear.run();
        db.exec("PRAGMA incremental_vacuum");
    };
    const close = () => {
        try {
            db.exec("PRAGMA wal_checkpoint(TRUNCATE)");
        }
        catch {
            // ignore
        }
        db.close?.();
    };
    const normalizedTranscriptNamespace = typeof transcriptNamespace === "string" && transcriptNamespace.trim().length > 0
        ? transcriptNamespace.trim()
        : null;
    const getTranscriptKey = (url, fileMtime) => buildTranscriptCacheKey({
        url,
        namespace: normalizedTranscriptNamespace,
        fileMtime,
    });
    const transcriptCache = {
        get: async ({ url, fileMtime }) => {
            const now = Date.now();
            const key = getTranscriptKey(url, fileMtime);
            const row = readEntry("transcript", key, now);
            if (!row)
                return null;
            const expired = typeof row.expires_at === "number" && row.expires_at <= now;
            let payload = null;
            try {
                payload = JSON.parse(row.value);
            }
            catch {
                payload = null;
            }
            const resourceKey = typeof payload?.resourceKey === "string" && payload.resourceKey.trim().length > 0
                ? payload.resourceKey.trim()
                : null;
            return {
                content: payload?.content ?? null,
                source: normalizeTranscriptSource(payload?.source) ?? null,
                expired,
                metadata: payload?.metadata ?? null,
                resourceKey,
            };
        },
        set: async ({ url, content, source, ttlMs, metadata, service, resourceKey, fileMtime }) => {
            const key = getTranscriptKey(url, fileMtime);
            setJson("transcript", key, {
                content,
                source,
                metadata: metadata ?? null,
                service,
                resourceKey,
                namespace: normalizedTranscriptNamespace,
                formatVersion: CACHE_FORMAT_VERSION,
            }, ttlMs);
        },
    };
    return { getText, getJson, setText, setJson, clear, close, transcriptCache };
}
export function clearCacheFiles(path) {
    rmSync(path, { force: true });
    rmSync(`${path}-wal`, { force: true });
    rmSync(`${path}-shm`, { force: true });
}
export function buildExtractCacheKey({ url, options, }) {
    return buildExtractCacheKeyValue({
        url,
        options,
        formatVersion: CACHE_FORMAT_VERSION,
    });
}
export function buildSummaryCacheKey({ contentHash, promptHash, model, lengthKey, languageKey, }) {
    return buildSummaryCacheKeyValue({
        contentHash,
        promptHash,
        model,
        lengthKey,
        languageKey,
        formatVersion: CACHE_FORMAT_VERSION,
    });
}
export function buildSlidesCacheKey({ url, settings, }) {
    return buildSlidesCacheKeyValue({
        url,
        settings,
        formatVersion: CACHE_FORMAT_VERSION,
    });
}
export function buildTranscriptCacheKey({ url, namespace, formatVersion, fileMtime, }) {
    return buildTranscriptCacheKeyValue({
        url,
        namespace,
        fileMtime,
        formatVersion: formatVersion ?? CACHE_FORMAT_VERSION,
    });
}
export async function readCacheStats(path) {
    if (!existsSync(path))
        return null;
    const db = await openSqlite(path);
    try {
        db.exec("PRAGMA query_only = ON");
    }
    catch {
        // ignore
    }
    const counts = createEmptyCacheCounts();
    const rows = db.prepare("SELECT kind, COUNT(*) AS count FROM cache_entries GROUP BY kind").all();
    for (const row of rows) {
        if (row?.kind && typeof row.count === "number" && row.kind in counts) {
            counts[row.kind] = row.count;
        }
    }
    const totalRow = db.prepare("SELECT COUNT(*) AS count FROM cache_entries").get();
    const totalEntries = typeof totalRow?.count === "number" ? totalRow.count : 0;
    db.close?.();
    return {
        path,
        sizeBytes: getSqliteFileSizeBytes(path),
        totalEntries,
        counts,
    };
}
export function getSqliteFileSizeBytes(path) {
    let total = 0;
    try {
        total += statSync(path).size;
    }
    catch {
        // ignore
    }
    try {
        total += statSync(`${path}-wal`).size;
    }
    catch {
        // ignore
    }
    try {
        total += statSync(`${path}-shm`).size;
    }
    catch {
        // ignore
    }
    return total;
}
//# sourceMappingURL=cache.js.map