/**
 * Snapshot export service — fleet-general write integrity layer
 * (item-3-snapshot-export).
 *
 * Dumps Neotoma entity state as a structured JSON document that any
 * fleet can diff against its own external state (MEMORY.md, a shadow
 * SQLite, a vector store, etc.). The exporter is deliberately fleet-
 * agnostic: filters accept the same classification axes the write path
 * exposes (`entity_types`, `observation_source`, attribution fields)
 * and the output preserves per-field provenance so drift reports can
 * answer "who believed this, at what trust tier, via what kind of
 * write".
 *
 * Fleet-specific projection (AIBTC MEMORY.md, LangGraph state dumps,
 * custom-adapter logs) lives in separate parsers registered with
 * `drift_comparison.ts`; this module produces the *canonical* Neotoma
 * shape.
 *
 * Design notes:
 *   * The export is append-only and does not mutate state. Reuses the
 *     same snapshot tables that power the runtime read path (no
 *     recomputation).
 *   * `attribution_fingerprint` and `observation_source_histogram` are
 *     computed per entity so downstream reports can flag "this entity
 *     is 100% unverified_client" without a second pass.
 *   * Output is a plain JSON document (no streaming); bulk exports are
 *     bounded by the caller via `limit` + `since` filters.
 */
import { db } from "../db.js";
export const DEFAULT_SNAPSHOT_EXPORT_LIMIT = 500;
/**
 * Produce a canonical JSON export of entity snapshots matching the
 * provided filter. Runs one query per stage (entities, snapshots,
 * observations) and joins in memory so callers can hook up to any
 * repository backend the `db` shim supports.
 */
export async function exportEntitySnapshots(filter) {
    const limit = filter.limit ?? DEFAULT_SNAPSHOT_EXPORT_LIMIT;
    let snapshotQuery = db.from("entity_snapshots").select("*").eq("user_id", filter.user_id);
    if (filter.entity_types && filter.entity_types.length > 0) {
        snapshotQuery = snapshotQuery.in("entity_type", filter.entity_types);
    }
    if (filter.since) {
        snapshotQuery = snapshotQuery.gte("last_observation_at", filter.since);
    }
    snapshotQuery = snapshotQuery.order("last_observation_at", { ascending: false }).limit(limit);
    const { data: snapshotsRaw, error: snapErr } = await snapshotQuery;
    if (snapErr) {
        throw new Error(`Failed to read entity_snapshots: ${snapErr.message}`);
    }
    const snapshots = (snapshotsRaw ?? []);
    if (snapshots.length === 0) {
        return buildDocument(filter, []);
    }
    const entityIds = snapshots.map((s) => s.entity_id);
    const { data: entitiesRaw, error: entityErr } = await db
        .from("entities")
        .select("id, entity_type, canonical_name")
        .in("id", entityIds);
    if (entityErr) {
        throw new Error(`Failed to read entities: ${entityErr.message}`);
    }
    const entityById = new Map((entitiesRaw ?? []).map((e) => [e.id, e]));
    const { data: observationsRaw, error: obsErr } = await db
        .from("observations")
        .select("id, entity_id, observation_source, provenance")
        .in("entity_id", entityIds)
        .eq("user_id", filter.user_id);
    if (obsErr) {
        throw new Error(`Failed to read observations: ${obsErr.message}`);
    }
    const observations = (observationsRaw ?? []);
    // The observation-level filters (observation_source / agent_sub /
    // attribution_tier) restrict which ENTITIES appear in the output:
    // an entity survives iff at least one of its observations matches.
    const obsByEntity = new Map();
    for (const row of observations) {
        const bucket = obsByEntity.get(row.entity_id) ?? [];
        bucket.push(row);
        obsByEntity.set(row.entity_id, bucket);
    }
    const keep = (entityId) => {
        if (!filter.observation_source && !filter.agent_sub && !filter.attribution_tier) {
            return true;
        }
        const rows = obsByEntity.get(entityId) ?? [];
        return rows.some((row) => {
            if (filter.observation_source && row.observation_source !== filter.observation_source) {
                return false;
            }
            if (filter.agent_sub || filter.attribution_tier) {
                const prov = coerceProvenance(row.provenance);
                if (filter.agent_sub && prov.agent_sub !== filter.agent_sub)
                    return false;
                if (filter.attribution_tier && prov.attribution_tier !== filter.attribution_tier) {
                    return false;
                }
            }
            return true;
        });
    };
    const exported = snapshots
        .filter((snap) => keep(snap.entity_id))
        .map((snap) => {
        const entity = entityById.get(snap.entity_id);
        const contributingObs = obsByEntity.get(snap.entity_id) ?? [];
        return {
            entity_id: snap.entity_id,
            entity_type: entity?.entity_type ?? snap.entity_type,
            schema_version: snap.schema_version,
            canonical_name: entity?.canonical_name ?? null,
            computed_at: snap.computed_at,
            last_observation_at: snap.last_observation_at,
            observation_count: snap.observation_count,
            snapshot: snap.snapshot,
            provenance: snap.provenance ?? {},
            observation_source_histogram: buildObservationSourceHistogram(contributingObs),
            attribution_fingerprint: buildAttributionFingerprint(contributingObs),
        };
    });
    return buildDocument(filter, exported);
}
function buildDocument(filter, entities) {
    return {
        schema_version: "0.1.0",
        exported_at: new Date().toISOString(),
        filter: { ...filter },
        total_entities: entities.length,
        entities,
    };
}
function coerceProvenance(raw) {
    if (raw && typeof raw === "object" && !Array.isArray(raw)) {
        return raw;
    }
    if (typeof raw === "string" && raw.length > 0) {
        try {
            const parsed = JSON.parse(raw);
            if (parsed && typeof parsed === "object")
                return parsed;
        }
        catch {
            // Malformed provenance blobs are treated as empty — better to
            // surface "no attribution" than crash the whole export.
            return {};
        }
    }
    return {};
}
function buildObservationSourceHistogram(rows) {
    const out = {};
    for (const row of rows) {
        const key = row.observation_source ?? "unclassified";
        out[key] = (out[key] ?? 0) + 1;
    }
    return out;
}
function buildAttributionFingerprint(rows) {
    const tiers = {};
    const agentSubs = new Set();
    const clientNames = new Set();
    let attributedCount = 0;
    for (const row of rows) {
        const prov = coerceProvenance(row.provenance);
        const tier = prov.attribution_tier;
        if (tier) {
            tiers[tier] = (tiers[tier] ?? 0) + 1;
        }
        if (typeof prov.agent_sub === "string" && prov.agent_sub.length > 0) {
            agentSubs.add(prov.agent_sub);
        }
        if (typeof prov.client_name === "string" && prov.client_name.length > 0) {
            clientNames.add(prov.client_name);
        }
        const hasAttribution = typeof prov.agent_sub === "string" ||
            typeof prov.agent_public_key === "string" ||
            typeof prov.client_name === "string" ||
            typeof prov.connection_id === "string";
        if (hasAttribution)
            attributedCount += 1;
    }
    return {
        tiers,
        agent_subs: [...agentSubs].sort(),
        client_names: [...clientNames].sort(),
        fully_attributed: rows.length > 0 && attributedCount === rows.length,
    };
}
//# sourceMappingURL=snapshot_export.js.map