import { getSession } from "../lib/neo4j.js";
import { notTrashed } from "../../../../../lib/graph-trash/dist/index.js";
import { existsSync, readFileSync } from "node:fs";
import { resolve } from "node:path";
// MCP runs in a separate node process; PLATFORM_ROOT is provided
// by the platform server when it spawns the MCP. Used to locate
// `data/accounts/<accountId>/account.json` for plugin-key resolution.
const PLATFORM_ROOT = process.env.MAXY_PLATFORM_ROOT ?? resolve(process.cwd(), "..");
// Cross-process agreement: these values must match neo4j-store.ts
const INJECTION_THRESHOLD = 0.4;
const MAX_SUMMARY_PREFERENCES = 30;
export async function profileRead(params) {
    const { accountId, userId, detail } = params;
    const session = getSession();
    try {
        // widen WHERE so a notApplicable Preference survives even
        // if a future bug ever sets confidence wrong. Spec sets confidence: 1.0
        // on notApplicable writes; the OR is belt-and-braces against drift.
        const result = await session.run(`
      OPTIONAL MATCH (up:UserProfile {accountId: $accountId, userId: $userId})
      WHERE ${notTrashed("up")}
      OPTIONAL MATCH (au:AdminUser {userId: $userId})-[:OWNS]->(p:Person {accountId: $accountId})
      OPTIONAL MATCH (up)-[:HAS_PREFERENCE]->(pref:Preference)
      WHERE (pref.confidence >= $threshold OR pref.notApplicable = true) AND ${notTrashed("pref")}
      WITH up, p, pref
      ORDER BY pref.confidence DESC
      WITH up, p, collect(pref) AS allPrefs
      WITH up, p, allPrefs[0..$limit] AS prefs
      RETURN up, p AS person, prefs
      `, {
            accountId,
            userId,
            threshold: INJECTION_THRESHOLD,
            limit: MAX_SUMMARY_PREFERENCES,
        });
        if (result.records.length === 0 || !result.records[0].get("up")) {
            if (detail) {
                return {
                    profile: null,
                    preferences: [],
                    summary: "No profile found.",
                    stats: { totalPreferences: 0, explicitCount: 0, behaviouralCount: 0, highConfidence: 0, lowConfidence: 0 },
                };
            }
            return { profile: null, preferences: [], summary: "No profile found." };
        }
        const record = result.records[0];
        const upNode = record.get("up");
        const profileProps = { ...upNode.properties };
        delete profileProps.embedding;
        const personNode = record.get("person");
        if (personNode?.properties) {
            // Person fields are the canonical source for human-readable name.
            if (personNode.properties.givenName !== undefined)
                profileProps.givenName = personNode.properties.givenName;
            if (personNode.properties.familyName !== undefined)
                profileProps.familyName = personNode.properties.familyName;
        }
        const prefNodes = record.get("prefs");
        const preferences = prefNodes.map((p) => ({
            preferenceId: p.properties.preferenceId,
            category: p.properties.category,
            key: p.properties.key,
            value: p.properties.value,
            confidence: p.properties.confidence,
            source: p.properties.source,
            observedAt: p.properties.observedAt,
            notApplicable: p.properties.notApplicable === true,
        }));
        if (!detail) {
            const extraTemplates = loadEnabledPluginProfileKeys(accountId);
            const summary = formatProfileSummary(profileProps, preferences, extraTemplates);
            return { profile: profileProps, preferences, summary };
        }
        // Detail mode: fetch evidence trail for each preference
        const prefIds = preferences.map((p) => p.preferenceId);
        const evidenceMap = new Map();
        if (prefIds.length > 0) {
            const evidenceResult = await session.run(`
        UNWIND $prefIds AS pid
        MATCH (pref:Preference {preferenceId: pid})-[:OBSERVED_IN]->(conv:Conversation)
        WHERE ${notTrashed("pref")} AND ${notTrashed("conv")}
        RETURN pref.preferenceId AS preferenceId,
               collect({name: conv.name, date: conv.createdAt}) AS evidence
        `, { prefIds });
            for (const r of evidenceResult.records) {
                const pid = r.get("preferenceId");
                const rawEvidence = r.get("evidence");
                evidenceMap.set(pid, rawEvidence.map((e) => ({ conversationName: e.name, date: e.date })));
            }
        }
        const detailedPreferences = preferences.map((p) => ({
            ...p,
            evidence: evidenceMap.get(p.preferenceId) ?? [],
        }));
        const stats = {
            totalPreferences: preferences.length,
            explicitCount: preferences.filter((p) => p.source === "explicit").length,
            behaviouralCount: preferences.filter((p) => p.source === "behavioural").length,
            highConfidence: preferences.filter((p) => p.confidence >= 0.7).length,
            lowConfidence: preferences.filter((p) => p.confidence < 0.5).length,
        };
        const summary = formatDetailedSummary(profileProps, detailedPreferences, stats);
        return { profile: profileProps, preferences: detailedPreferences, summary, stats };
    }
    finally {
        await session.close();
    }
}
const CATEGORY_LABELS = {
    communication: "Communication",
    scheduling: "Scheduling",
    decision: "Decisions",
    workflow: "Workflow",
    content: "Content",
    interaction: "Interaction patterns",
};
// Canonical six. Mutating this list is a schema change — keep aligned with
// PROFILE_CATEGORIES in neo4j-store.ts.
const PROFILE_CATEGORIES = [
    "communication",
    "scheduling",
    "decision",
    "workflow",
    "content",
    "interaction",
];
// canonical per-category Preference.key enumeration. Consumed by
// `computeAbsentFields` for the sparse-state observability log only (Task
// 182: never reaches the agent-facing prose body). A field is "covered"
// when at least one Preference exists with matching `(category, key)` AND
// (`confidence >= INJECTION_THRESHOLD` OR `notApplicable = true`).
//
// Adding a field is a one-line edit — no migration. Existing organic
// Preferences whose `key` doesn't match a template field surface in the
// prose summary via their value; they simply don't shrink the log signal.
// template-driven, not graph-derived.
//
// Lockstep duplicate of the same literal in
// platform/plugins/memory/mcp/src/tools/profile-read.ts. The cross-process
// MCP runs in a separate node process AND vitest's mock boundary makes
// shared-module import unsafe; the `PROFILE_FIELD_TEMPLATES — cross-process
// lockstep` test in load-user-profile-sparse.test.ts asserts byte-equality
// of the literal across both files.
//
// Read-side bias for the agent (per IDENTITY.md § Conversational Memory):
// when satisfying a Missing field, the agent uses the EXACT `category` and
// `key` strings here as the MERGE-key on `profile-update`. Otherwise the
// organic key never shrinks Coverage and the user is re-asked.
export const PROFILE_FIELD_TEMPLATES = {
    communication: [
        "preferredChannel",
        "quietHours",
        "responseLatencyExpectation",
        "formalityLevel",
        "escalationPath",
    ],
    scheduling: [
        "workdayStartTime",
        "workdayEndTime",
        "weekendAvailability",
        "meetingBlockPreference",
        "focusBlockTiming",
    ],
    decision: [
        "riskTolerance",
        "autonomyBoundary",
        "evidenceThreshold",
        "reversibilityPreference",
        "stakeholderConsultation",
    ],
    workflow: [
        "batchingPreference",
        "taskGranularity",
        "deepWorkBlocks",
        "statusUpdateCadence",
        "toolingChoices",
    ],
    content: [
        "outputFormat",
        "lengthPreference",
        "citationStyle",
        "tonality",
    ],
    interaction: [
        "addressForm",
        "humourTolerance",
        "directnessLevel",
        "emotionalRegisterPreference",
    ],
};
// Compute the absent fields list (`category.key`) given the live preferences.
// A field is covered when at least one Preference matches its (category, key)
// AND is either above the injection threshold OR has notApplicable: true.
// `extraTemplates` carries plugin-contributed categories merged
// into the canonical six; canonical wins on overlap.
function computeAbsentFields(preferences, extraTemplates = {}) {
    const covered = new Set();
    for (const pref of preferences) {
        const isCovered = pref.notApplicable === true || pref.confidence >= INJECTION_THRESHOLD;
        if (isCovered)
            covered.add(`${pref.category}.${pref.key}`);
    }
    const absent = [];
    for (const [category, fields] of Object.entries(PROFILE_FIELD_TEMPLATES)) {
        for (const field of fields) {
            const dotted = `${category}.${field}`;
            if (!covered.has(dotted))
                absent.push(dotted);
        }
    }
    for (const [category, fields] of Object.entries(extraTemplates)) {
        if (category in PROFILE_FIELD_TEMPLATES)
            continue;
        for (const field of fields) {
            const dotted = `${category}.${field}`;
            if (!covered.has(dotted))
                absent.push(dotted);
        }
    }
    return absent;
}
// parse the `profileKeys:` YAML list from a PLUGIN.md frontmatter
// block. Returns dotted key strings in declaration order. Empty array on any
// failure (no frontmatter, no `profileKeys:` field, malformed line). Public
// for unit tests; production callers use `loadEnabledPluginProfileKeys`.
//
// Lockstep duplicate: identical literal lives in
// platform/ui/app/lib/neo4j-store.ts (the MCP runs in a separate node
// process and vitest's mock boundary makes shared-module import unsafe,
// same rationale as PROFILE_FIELD_TEMPLATES above).
export function parsePluginProfileKeys(manifest) {
    const fm = manifest.match(/^---\n([\s\S]*?)\n---/);
    if (!fm)
        return [];
    const out = [];
    let inBlock = false;
    for (const line of fm[1].split("\n")) {
        if (/^profileKeys:/.test(line)) {
            inBlock = true;
            continue;
        }
        if (inBlock) {
            const m = line.match(/^\s+-\s+(.+?)\s*$/);
            if (m)
                out.push(m[1]);
            else
                break;
        }
    }
    return out;
}
// read every enabled plugin's `profileKeys:` declaration from its
// PLUGIN.md frontmatter and group by category prefix. Used at Coverage-format
// time to extend the canonical-six dictionary without perturbing it. Loud
// failures: account.json absent → `{}` + logged warning; plugin manifest
// missing → silently skipped (the missing-plugin signal lives in
// findMissingPlugins / GET /api/health, not here); malformed dotted key
// (no `.`, empty prefix, empty suffix) → skipped + logged.
export function loadEnabledPluginProfileKeys(accountId) {
    const accountFile = resolve(PLATFORM_ROOT, "..", "data/accounts", accountId, "account.json");
    if (!existsSync(accountFile))
        return {};
    let enabled;
    try {
        const raw = readFileSync(accountFile, "utf-8");
        const parsed = JSON.parse(raw);
        enabled = Array.isArray(parsed.enabledPlugins)
            ? parsed.enabledPlugins.filter((p) => typeof p === "string")
            : [];
    }
    catch (err) {
        console.error(`[profile-keys] account.json read failed for ${accountId.slice(0, 8)}…: ${err instanceof Error ? err.message : String(err)}`);
        return {};
    }
    const merged = {};
    for (const pluginName of enabled) {
        const manifestPath = resolve(PLATFORM_ROOT, "plugins", pluginName, "PLUGIN.md");
        if (!existsSync(manifestPath))
            continue;
        let manifest;
        try {
            manifest = readFileSync(manifestPath, "utf-8");
        }
        catch {
            continue;
        }
        for (const dotted of parsePluginProfileKeys(manifest)) {
            const dot = dotted.indexOf(".");
            if (dot <= 0 || dot === dotted.length - 1) {
                console.error(`[profile-keys] malformed dotted key in ${pluginName}: ${dotted}`);
                continue;
            }
            const category = dotted.slice(0, dot);
            const key = dotted.slice(dot + 1);
            if (!merged[category])
                merged[category] = new Set();
            merged[category].add(key);
        }
    }
    const out = {};
    for (const [cat, set] of Object.entries(merged))
        out[cat] = [...set];
    return out;
}
// Profile summary format — also implemented in neo4j-store.ts (loadUserProfile).
// Changes here must be reflected there and vice versa.
//
// Task 182: principle-not-prescription body. See the neo4j-store.ts copy
// for the rationale; this implementation must stay byte-equivalent for the
// canonical inputs the parity test exercises.
export function formatProfileSummary(profile, preferences, 
// eslint-disable-next-line @typescript-eslint/no-unused-vars
extraTemplates = {}) {
    const summary = composeOperatorSummary(profile, preferences);
    return [
        `This is what you know about the operator: ${summary}.`,
        `You must seek to MAXIMISE what you know about the operator to do your job effectively.`,
    ].join("\n");
}
function composeOperatorSummary(profile, preferences) {
    const facts = [];
    const givenName = typeof profile.givenName === "string" ? profile.givenName.trim() : "";
    const familyName = typeof profile.familyName === "string" ? profile.familyName.trim() : "";
    const fullName = [givenName, familyName].filter((s) => s.length > 0).join(" ");
    if (fullName)
        facts.push(`name ${fullName}`);
    if (typeof profile.role === "string" && profile.role.trim()) {
        facts.push(`role ${profile.role.trim()}`);
    }
    if (typeof profile.timezone === "string" && profile.timezone.trim()) {
        facts.push(`timezone ${profile.timezone.trim()}`);
    }
    if (typeof profile.locale === "string" && profile.locale.trim()) {
        facts.push(`locale ${profile.locale.trim()}`);
    }
    if (typeof profile.email === "string" && profile.email.trim()) {
        facts.push(`email ${profile.email.trim()}`);
    }
    if (typeof profile.telephone === "string" && profile.telephone.trim()) {
        facts.push(`telephone ${profile.telephone.trim()}`);
    }
    if (profile.expertise) {
        const expVal = typeof profile.expertise === "string"
            ? profile.expertise
            : JSON.stringify(profile.expertise);
        if (expVal.trim())
            facts.push(`expertise ${expVal.trim()}`);
    }
    const byCategory = {};
    for (const pref of preferences) {
        if (pref.notApplicable === true)
            continue;
        if (typeof pref.value !== "string" || pref.value.trim().length === 0)
            continue;
        if (!byCategory[pref.category])
            byCategory[pref.category] = [];
        byCategory[pref.category].push(pref.value.trim());
    }
    for (const [cat, vals] of Object.entries(byCategory)) {
        facts.push(`${cat} ${vals.join(", ")}`);
    }
    return facts.length > 0 ? facts.join("; ") : "NOTHING";
}
// Detailed summary for "what do you know about me?" — includes confidence,
// source, and evidence trail per preference.
function formatDetailedSummary(profile, preferences, stats) {
    const givenName = typeof profile.givenName === "string" ? profile.givenName.trim() : "";
    const familyName = typeof profile.familyName === "string" ? profile.familyName.trim() : "";
    const joined = familyName ? `${givenName} ${familyName}` : givenName;
    const name = joined || "Owner";
    const role = profile.role ? ` (${profile.role})` : "";
    const tz = profile.timezone ? ` — ${profile.timezone}` : "";
    const locale = profile.locale ? `, ${profile.locale}` : "";
    const expertise = profile.expertise
        ? `\nExpertise: ${typeof profile.expertise === "string"
            ? profile.expertise
            : JSON.stringify(profile.expertise)}`
        : "";
    const lines = [`## What I Know About You\n`];
    lines.push(`${name}${role}${tz}${locale}${expertise}\n`);
    lines.push(`Memory: ${stats.totalPreferences} preferences ` +
        `(${stats.explicitCount} you told me, ${stats.behaviouralCount} I observed). ` +
        `${stats.highConfidence} high-confidence, ${stats.lowConfidence} fading.\n`);
    // Group by category
    const categories = {};
    for (const pref of preferences) {
        if (!categories[pref.category])
            categories[pref.category] = [];
        categories[pref.category].push(pref);
    }
    const allCategories = ["communication", "scheduling", "decision", "workflow", "content", "interaction"];
    for (const cat of allCategories) {
        if (!categories[cat])
            continue;
        const label = CATEGORY_LABELS[cat] || cat;
        lines.push(`### ${label}`);
        for (const pref of categories[cat]) {
            const conf = confidenceLabel(pref.confidence);
            const src = pref.source === "explicit" ? "you said" : "observed";
            const date = pref.observedAt ? ` (${pref.observedAt.slice(0, 10)})` : "";
            lines.push(`- **${pref.key}**: ${pref.value} — ${conf}, ${src}${date}`);
            if (pref.evidence.length > 0) {
                const evidenceStrs = pref.evidence
                    .map((e) => e.conversationName || e.date?.slice(0, 10) || "unknown")
                    .slice(0, 3);
                lines.push(`  Evidence: ${evidenceStrs.join(", ")}`);
            }
        }
        lines.push("");
    }
    return lines.join("\n");
}
function confidenceLabel(confidence) {
    if (confidence >= 0.8)
        return "strong";
    if (confidence >= 0.6)
        return "moderate";
    if (confidence >= 0.4)
        return "fading";
    return "weak";
}
//# sourceMappingURL=profile-read.js.map