/**
 * Historical backfill helper for the compliance scorecard.
 *
 * Early Neotoma deployments collected `conversation_message` and tool
 * traffic for months before the per-turn `turn_compliance` entity was
 * introduced. To give those operators meaningful numbers without
 * re-running historical traffic through the hooks, this module
 * synthesizes `turn_compliance` snapshots from existing
 * `conversation_message` + tool-invocation history.
 *
 * The heuristic, in plain English:
 *   - Group `conversation_message{role:'user'}` rows by their
 *     `(conversation_id, turn_id)` (or, when missing, by `turn_key`).
 *   - For each user message, look for an `conversation_message{role:'assistant'}`
 *     in the same turn. When found, look for a `store_structured` call
 *     attributable to that turn (best-effort: any `agent_message`
 *     observation or `entity` whose creation timestamp falls in the same
 *     ±15min window and whose source/agent matches).
 *   - When no such call is found, mark the turn as
 *     `status: "backfilled_by_hook"` with `missed_steps:
 *     ["store_structured (estimated)"]`.
 *
 * Each synthesized row carries `estimated: true`, so the scorecard
 * renderer dims the row and the operator knows the number is an estimate.
 *
 * The helper is idempotent: it never touches turns that already have a
 * `conversation_turn` / `turn_compliance` / `turn_activity` row. Re-running
 * is safe.
 *
 * This is NOT a long-term fix: the right answer is to install hooks. The
 * helper merely provides day-zero numbers; subsequent live traffic will
 * naturally replace estimates with real measurements.
 */
import { getSqliteDb } from "../../repositories/sqlite/sqlite_client.js";
import { logger } from "../../utils/logger.js";
const TS_EPOCH = "1970-01-01T00:00:00.000Z";
/**
 * Re-derive `conversation_turn` rows from existing
 * `conversation_message` data when no per-turn entity exists. Returns a
 * summary; throws on hard failures (e.g. DB unreadable). Soft warnings
 * (e.g. cannot resolve a particular turn) are aggregated in the result.
 */
export function synthesizeHistoricalTurnCompliance(options) {
    const db = getSqliteDb();
    const since = options.since?.trim() || null;
    const until = options.until?.trim() || null;
    const maxTurns = Math.max(1, options.max_turns ?? 5_000);
    const dryRun = options.dry_run === true;
    const defaultModel = options.default_model ?? "(historical-unknown)";
    const defaultHarness = options.default_harness ?? "historical-estimate";
    const warnings = [];
    let examined = 0;
    let estimatedBackfilled = 0;
    let estimatedCompliant = 0;
    let written = 0;
    const stmt = db.prepare(`
SELECT
  e.id AS entity_id,
  json_extract(es.snapshot, '$.conversation_id') AS conversation_id,
  json_extract(es.snapshot, '$.turn_id') AS turn_id,
  json_extract(es.snapshot, '$.turn_key') AS turn_key,
  json_extract(es.snapshot, '$.role') AS role,
  json_extract(es.snapshot, '$.observed_at') AS observed_at,
  e.created_at AS created_at,
  json_extract(es.snapshot, '$.model') AS model,
  json_extract(es.snapshot, '$.harness') AS harness
FROM entities e
LEFT JOIN entity_snapshots es ON es.entity_id = e.id
WHERE e.user_id = ?
  AND e.entity_type IN ('conversation_message', 'agent_message')
  AND (e.merged_to_entity_id IS NULL OR trim(ifnull(e.merged_to_entity_id, '')) = '')
ORDER BY e.created_at ASC
`);
    // Build a set of existing turn_keys so we never double-write.
    const existingTurnKeys = new Set();
    const existingStmt = db.prepare(`
SELECT json_extract(es.snapshot, '$.turn_key') AS turn_key
FROM entities e
JOIN entity_snapshots es ON es.entity_id = e.id
WHERE e.user_id = ?
  AND e.entity_type IN ('conversation_turn', 'turn_compliance', 'turn_activity')
`);
    const existingIterate = existingStmt.iterate;
    const existingRows = typeof existingIterate === "function"
        ? existingIterate.call(existingStmt, options.user_id)
        : existingStmt.all(options.user_id);
    for (const row of existingRows) {
        if (row.turn_key)
            existingTurnKeys.add(row.turn_key);
    }
    const turns = new Map();
    const messageIterate = stmt.iterate;
    const messageRows = typeof messageIterate === "function"
        ? messageIterate.call(stmt, options.user_id)
        : stmt.all(options.user_id);
    for (const raw of messageRows) {
        const ts = (raw.observed_at ?? "").trim() || (raw.created_at ?? "").trim() || TS_EPOCH;
        if (since && ts < since)
            continue;
        if (until && ts > until)
            continue;
        const conversationId = (raw.conversation_id ?? "").trim() || null;
        const turnId = (raw.turn_id ?? "").trim() || null;
        const turnKey = (raw.turn_key ?? "").trim() ||
            (conversationId && turnId ? `${conversationId}:${turnId}` : null);
        if (!turnKey) {
            warnings.push(`message ${raw.entity_id} has no turn_key`);
            continue;
        }
        if (existingTurnKeys.has(turnKey))
            continue;
        let state = turns.get(turnKey);
        if (!state) {
            if (turns.size >= maxTurns)
                break;
            state = {
                turn_key: turnKey,
                conversation_id: conversationId,
                turn_id: turnId,
                user_seen: false,
                assistant_seen: false,
                user_ts: null,
                assistant_ts: null,
                model: (raw.model ?? "").trim() || null,
                harness: (raw.harness ?? "").trim() || null,
            };
            turns.set(turnKey, state);
        }
        const role = (raw.role ?? "").trim().toLowerCase();
        if (role === "user") {
            state.user_seen = true;
            state.user_ts = state.user_ts ?? ts;
        }
        else if (role === "assistant" || role === "agent") {
            state.assistant_seen = true;
            state.assistant_ts = state.assistant_ts ?? ts;
        }
        if (!state.model && raw.model)
            state.model = (raw.model ?? "").trim() || null;
        if (!state.harness && raw.harness)
            state.harness = (raw.harness ?? "").trim() || null;
    }
    // For each candidate turn, determine if a `store_structured` call
    // happened in the same window. Cheap heuristic: count any entity
    // with created_at within the turn window EXCLUDING the bookkeeping
    // types (conversation, conversation_message, agent_message).
    const windowStmt = db.prepare(`
SELECT count(*) AS cnt
FROM entities e
WHERE e.user_id = ?
  AND e.entity_type NOT IN ('conversation', 'conversation_message', 'agent_message', 'conversation_turn', 'turn_compliance', 'turn_activity')
  AND e.created_at >= ?
  AND e.created_at <= ?
`);
    for (const state of turns.values()) {
        examined += 1;
        const userTs = state.user_ts ?? state.assistant_ts ?? null;
        const assistantTs = state.assistant_ts ?? state.user_ts ?? null;
        if (!userTs || !assistantTs) {
            // Incomplete turn (user-only or assistant-only): treat as
            // compliant if the agent simply did not respond yet.
            estimatedCompliant += 1;
            continue;
        }
        const lower = new Date(new Date(userTs).getTime() - 60_000).toISOString();
        const upper = new Date(new Date(assistantTs).getTime() + 15 * 60_000).toISOString();
        const row = windowStmt.get(options.user_id, lower, upper);
        const stored = (row?.cnt ?? 0) > 0;
        if (stored) {
            estimatedCompliant += 1;
        }
        else {
            estimatedBackfilled += 1;
        }
        if (dryRun)
            continue;
        if (existingTurnKeys.has(state.turn_key))
            continue;
        try {
            writeSyntheticTurn(db, {
                user_id: options.user_id,
                turn_key: state.turn_key,
                conversation_id: state.conversation_id,
                turn_id: state.turn_id,
                backfilled: !stored,
                model: state.model ?? defaultModel,
                harness: state.harness ?? defaultHarness,
                started_at: userTs,
                ended_at: assistantTs,
            });
            written += 1;
            existingTurnKeys.add(state.turn_key);
        }
        catch (err) {
            const message = err.message;
            warnings.push(`turn_key ${state.turn_key}: ${message}`);
            logger.warn(`[compliance/historical-backfill] failed to write ${state.turn_key}: ${message}`);
        }
    }
    return {
        examined_turns: examined,
        estimated_backfilled: estimatedBackfilled,
        estimated_compliant: estimatedCompliant,
        written,
        dry_run: dryRun,
        warnings,
    };
}
function writeSyntheticTurn(db, payload) {
    const entityId = `historical-${hashString(payload.turn_key)}`;
    const snapshot = JSON.stringify({
        turn_key: payload.turn_key,
        conversation_id: payload.conversation_id,
        turn_id: payload.turn_id,
        harness: payload.harness,
        model: payload.model,
        status: payload.backfilled ? "backfilled_by_hook" : "compliant_by_agent",
        missed_steps: payload.backfilled ? ["store_structured (estimated)"] : [],
        store_structured_calls: payload.backfilled ? 0 : 1,
        started_at: payload.started_at,
        ended_at: payload.ended_at,
        source: "historical-estimate",
        estimated: 1,
    });
    const now = new Date().toISOString();
    // Insert into entities + entity_snapshots in one tx. We deliberately
    // avoid the higher-level reducer here because the synthetic row
    // should never trigger downstream side effects (timeline events,
    // search index, etc.). It is a read-only projection.
    db.prepare(`INSERT OR IGNORE INTO entities (id, user_id, entity_type, canonical_name, created_at, updated_at)
     VALUES (?, ?, 'turn_compliance', ?, ?, ?)`).run(entityId, payload.user_id, payload.turn_key, now, now);
    db.prepare(`INSERT OR REPLACE INTO entity_snapshots (
       entity_id, user_id, entity_type, schema_version, snapshot, computed_at,
       observation_count, last_observation_at, provenance
     ) VALUES (?, ?, 'turn_compliance', 1, ?, ?, 1, ?, json('{"observation_source":"import","estimated":1}'))`).run(entityId, payload.user_id, snapshot, now, now);
}
function hashString(input) {
    let hash = 0;
    for (let i = 0; i < input.length; i++) {
        hash = (Math.imul(31, hash) + input.charCodeAt(i)) | 0;
    }
    return Math.abs(hash).toString(16).padStart(8, "0");
}
//# sourceMappingURL=historical_backfill.js.map