/**
 * Phase 7 — tag deduplication and normalisation (Task 596).
 *
 * Two passes:
 *
 * Pass A — KnowledgeDocument nodes with a `tags` string-array property.
 *   For each node, normalise every tag (lowercase, strip non-alphanumeric
 *   except hyphens/underscores/spaces, trim), deduplicate, and write back
 *   only when the normalised array differs from the original.
 *
 * Pass B — :DefinedTerm {category:'obsidian-tag'} nodes.
 *   Group by normalised name. For each group with more than one variant,
 *   pick the first alphabetically as canonical, retarget all :TAGGED_WITH
 *   edges from non-canonical nodes to the canonical node, then detach-delete
 *   non-canonical nodes that become edge-free.
 *
 * `applied` = number of KD nodes updated + number of DefinedTerm non-canonical
 *   variants whose :TAGGED_WITH edges were retargeted (whether or not the node
 *   was subsequently deleted — deletion is blocked when other edges exist).
 */
/** Normalise a tag string: lowercase, strip non-alphanumeric-separator chars, trim. */
export function normaliseTagStr(tag) {
    return tag
        .toLowerCase()
        .replace(/[^a-z0-9_\- ]/g, "")
        .trim();
}
export async function phaseTagNormalisation(ctx) {
    const start = Date.now();
    const { accountId, session } = ctx;
    let applied = 0;
    // --- Pass A: KnowledgeDocument tags[] array ---
    const kdResult = await session.run(`MATCH (n:KnowledgeDocument)
     WHERE n.accountId = $accountId AND n.tags IS NOT NULL AND size(n.tags) > 0
     RETURN elementId(n) AS eid, n.tags AS tags`, { accountId });
    for (const record of kdResult.records) {
        const eid = record.get("eid");
        const tags = record.get("tags");
        const normTags = [...new Set(tags.map(normaliseTagStr).filter((t) => t.length > 0))];
        if (normTags.length !== tags.length || normTags.some((t, i) => t !== tags[i])) {
            await session.run(`MATCH (n) WHERE elementId(n) = $eid SET n.tags = $tags`, { eid, tags: normTags });
            applied++;
        }
    }
    // --- Pass B: DefinedTerm obsidian-tag variants ---
    const dtResult = await session.run(`MATCH (t:DefinedTerm)
     WHERE t.accountId = $accountId AND t.category = 'obsidian-tag'
     RETURN elementId(t) AS eid, t.name AS name`, { accountId });
    const groups = new Map();
    for (const record of dtResult.records) {
        const eid = record.get("eid");
        const name = record.get("name");
        const key = normaliseTagStr(name);
        if (!groups.has(key))
            groups.set(key, []);
        groups.get(key).push({ eid, name });
    }
    for (const [, variants] of groups) {
        if (variants.length <= 1)
            continue;
        // Canonical = alphabetically first (stable, deterministic).
        variants.sort((a, b) => a.name.localeCompare(b.name));
        const canonical = variants[0];
        const nonCanonicals = variants.slice(1);
        for (const nonCanon of nonCanonicals) {
            await session.run(`MATCH (t:DefinedTerm) WHERE elementId(t) = $oldEid
         MATCH (canon:DefinedTerm) WHERE elementId(canon) = $canonEid
         OPTIONAL MATCH (src)-[r:TAGGED_WITH]->(t)
         FOREACH (_ IN CASE WHEN src IS NOT NULL THEN [1] ELSE [] END |
           MERGE (src)-[:TAGGED_WITH]->(canon)
           DELETE r
         )
         WITH t
         WHERE NOT (t)-[]-()
         DETACH DELETE t`, { oldEid: nonCanon.eid, canonEid: canonical.eid });
            applied++;
        }
    }
    const elapsedMs = Date.now() - start;
    process.stderr.write(`[dream-cycle] phase=7 op=tags-normalised accountId=${accountId} merged=${applied} elapsedMs=${elapsedMs}\n`);
    return {
        phase: 7,
        name: "tag-normalisation",
        durationSec: Math.round(elapsedMs / 1000),
        proposed: applied,
        applied,
        deferred: 0,
    };
}
//# sourceMappingURL=phase-tag-normalisation.js.map