// Pure row↔record translation. The row shape mirrors the schema.ts column layout 1:1; the record
// shape is the contract's MemoryRecord. We keep both layers in this single module so a schema
// column rename has exactly one place to update.
//
// Encoding choices:
//   - tags          → JSON array string
//   - payload       → JSON-stringified discriminated union (NULL when absent)
//   - validUntil    → NULL when absent (NOT undefined-as-string)
//   - staleReason   → NULL when absent
//   - retentionHint → flattened across three columns; NULL all when absent
//   - sensitivity   → flat column (validator treats it as a provenance attribute but storing it
//                     flat keeps the scoped-list query free of payload parsing for the common case)
import { scopeCoordinateOf, scopeKindOf } from "./scope-key.js";
import { MemoryStorageError } from "./errors.js";
// Content-vs-metadata split (ADR-0035): only the columns that carry remembered TEXT are sealed —
// body, payload_json, tags_json, capture_rationale, stale_reason. Every other column (ids, scope,
// status, sensitivity, pinned, confidence, all timestamps, source_*, model_*, retention_*) stays
// cleartext so SQL indexes and the UI scope display keep working without a key. Crypto is confined
// to the two wrapper functions below; the pure row↔record builders never see a key.
function openNullable(cipher, value) {
    return value === null ? null : cipher.openString(value);
}
// Returns a row whose sealed content columns are decrypted in place, so the pure builders below run
// on plaintext exactly as they did before encryption landed.
function decryptContentColumns(row, cipher) {
    return {
        ...row,
        body: cipher.openString(row.body),
        payload_json: openNullable(cipher, row.payload_json),
        tags_json: cipher.openString(row.tags_json),
        capture_rationale: openNullable(cipher, row.capture_rationale),
        stale_reason: openNullable(cipher, row.stale_reason),
    };
}
function sealNullable(cipher, value) {
    return value === null ? null : cipher.sealString(value);
}
// Returns a write-row whose content columns are sealed, leaving every metadata column untouched.
function encryptContentColumns(row, cipher) {
    return {
        ...row,
        body: cipher.sealString(row.body),
        payload_json: sealNullable(cipher, row.payload_json),
        tags_json: cipher.sealString(row.tags_json),
        capture_rationale: sealNullable(cipher, row.capture_rationale),
        stale_reason: sealNullable(cipher, row.stale_reason),
    };
}
function buildScopeFromRow(kind, coord) {
    switch (kind) {
        case "user":
            return { kind: "user", userId: coord };
        case "workspace":
            return { kind: "workspace", workspaceId: coord };
        case "project":
            return { kind: "project", projectId: coord };
        case "workflow":
            return { kind: "workflow", workflowDefinitionId: coord };
        case "global":
            return { kind: "global" };
        default:
            throw new MemoryStorageError("schema-mismatch", "Stored memory scope kind is not recognized.");
    }
}
function buildProvenanceFromRow(row) {
    const base = {
        sourceKind: row.source_kind,
        capturedAt: row.captured_at,
        confidence: row.confidence,
        sensitivity: row.sensitivity,
    };
    if (row.source_conversation_id !== null) {
        base.sourceConversationId = row.source_conversation_id;
    }
    if (row.source_workflow_run_id !== null) {
        base.sourceWorkflowRunId = row.source_workflow_run_id;
    }
    if (row.source_evidence_manifest_id !== null) {
        base.sourceEvidenceManifestId = row.source_evidence_manifest_id;
    }
    if (row.capture_rationale !== null) {
        base.captureRationale = row.capture_rationale;
    }
    if (row.model_provider !== null && row.model_id !== null) {
        const identity = {
            provider: row.model_provider,
            modelId: row.model_id,
        };
        if (row.model_revision !== null)
            identity.modelRevision = row.model_revision;
        base.modelIdentity = identity;
    }
    return base;
}
function buildValidityFromRow(row) {
    return row.valid_until === null
        ? { validFrom: row.valid_from }
        : { validFrom: row.valid_from, validUntil: row.valid_until };
}
function buildRetentionHintFromRow(row) {
    if (row.retention_policy_key === null)
        return undefined;
    const hint = {
        policyKey: row.retention_policy_key,
    };
    if (row.retention_retain_until !== null)
        hint.retainUntil = row.retention_retain_until;
    if (row.retention_notes !== null)
        hint.notes = row.retention_notes;
    return hint;
}
function parseTagsJson(raw) {
    let parsed;
    try {
        parsed = JSON.parse(raw);
    }
    catch {
        throw new MemoryStorageError("schema-mismatch", "Stored memory tags JSON is invalid.");
    }
    if (!Array.isArray(parsed))
        return [];
    return parsed.filter((v) => typeof v === "string");
}
function parsePayloadJson(raw) {
    if (raw === null)
        return undefined;
    try {
        return JSON.parse(raw);
    }
    catch {
        throw new MemoryStorageError("schema-mismatch", "Stored memory payload JSON is invalid.");
    }
}
export function rowToMemoryRecord(row, cipher) {
    const decrypted = decryptContentColumns(row, cipher);
    return decryptedRowToMemoryRecord(decrypted);
}
function decryptedRowToMemoryRecord(row) {
    const payload = parsePayloadJson(row.payload_json);
    const retentionHint = buildRetentionHintFromRow(row);
    const base = {
        id: row.id,
        schemaVersion: "1",
        scope: buildScopeFromRow(row.scope_kind, row.scope_coordinate),
        type: row.type,
        body: row.body,
        provenance: buildProvenanceFromRow(row),
        validity: buildValidityFromRow(row),
        status: row.status,
        pinned: row.pinned === 1,
        tags: parseTagsJson(row.tags_json),
        createdAt: row.created_at,
        updatedAt: row.updated_at,
    };
    const out = {
        ...base,
        ...(payload !== undefined ? { payload } : {}),
        ...(row.stale_reason !== null ? { staleReason: row.stale_reason } : {}),
        ...(retentionHint !== undefined ? { retentionHint } : {}),
    };
    return out;
}
function modelIdentityFieldsToRow(identity) {
    return {
        model_provider: identity?.provider ?? null,
        model_id: identity?.modelId ?? null,
        model_revision: identity?.modelRevision ?? null,
    };
}
function provenanceFieldsToRow(prov) {
    return {
        sensitivity: prov.sensitivity,
        confidence: prov.confidence,
        source_kind: prov.sourceKind,
        source_conversation_id: prov.sourceConversationId ?? null,
        source_workflow_run_id: prov.sourceWorkflowRunId ?? null,
        source_evidence_manifest_id: prov.sourceEvidenceManifestId ?? null,
        captured_at: prov.capturedAt,
        capture_rationale: prov.captureRationale ?? null,
        ...modelIdentityFieldsToRow(prov.modelIdentity),
    };
}
function retentionFieldsToRow(hint) {
    return {
        retention_policy_key: hint?.policyKey ?? null,
        retention_retain_until: hint?.retainUntil ?? null,
        retention_notes: hint?.notes ?? null,
    };
}
export function memoryRecordToRow(record, cipher) {
    return encryptContentColumns(plainRecordToRow(record), cipher);
}
function plainRecordToRow(record) {
    return {
        id: record.id,
        schema_version: record.schemaVersion,
        type: record.type,
        scope_kind: scopeKindOf(record.scope),
        scope_coordinate: scopeCoordinateOf(record.scope),
        body: record.body,
        payload_json: record.payload === undefined ? null : JSON.stringify(record.payload),
        status: record.status,
        pinned: record.pinned ? 1 : 0,
        valid_from: record.validity.validFrom,
        valid_until: record.validity.validUntil ?? null,
        stale_reason: record.staleReason ?? null,
        tags_json: JSON.stringify(record.tags),
        ...provenanceFieldsToRow(record.provenance),
        ...retentionFieldsToRow(record.retentionHint),
        created_at: record.createdAt,
        updated_at: record.updatedAt,
    };
}
