"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.mirrorNoteCreate = mirrorNoteCreate;
exports.mirrorNoteUpdate = mirrorNoteUpdate;
exports.mirrorTaskCreate = mirrorTaskCreate;
exports.mirrorTaskUpdate = mirrorTaskUpdate;
exports.mirrorSkillCreate = mirrorSkillCreate;
exports.mirrorSkillUpdate = mirrorSkillUpdate;
exports.mirrorNoteRelation = mirrorNoteRelation;
exports.mirrorTaskRelation = mirrorTaskRelation;
exports.mirrorSkillRelation = mirrorSkillRelation;
exports.mirrorAttachmentEvent = mirrorAttachmentEvent;
exports.deleteMirrorDir = deleteMirrorDir;
exports.sanitizeEntityId = sanitizeEntityId;
exports.sanitizeFilename = sanitizeFilename;
exports.writeAttachment = writeAttachment;
exports.deleteAttachment = deleteAttachment;
exports.getAttachmentPath = getAttachmentPath;
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const crypto_1 = __importDefault(require("crypto"));
const frontmatter_1 = require("./frontmatter");
const events_log_1 = require("./events-log");
const logger_1 = require("../lib/logger");
const log = (0, logger_1.createLogger)('file-mirror');
/** Sanitize a string for safe inclusion in log output. */
function sanitizeForLog(s) {
    return s.replace(/[\r\n\t]/g, ' ').slice(0, 200);
}
/** Write to a temp file then rename — atomic on same filesystem. */
function atomicWriteFileSync(filePath, data, encoding) {
    const tmp = `${filePath}.${crypto_1.default.randomBytes(4).toString('hex')}.tmp`;
    fs.writeFileSync(tmp, data, encoding);
    fs.renameSync(tmp, filePath);
}
function tsToIso(ts) {
    if (ts == null || ts === 0)
        return null;
    return new Date(ts).toISOString();
}
function buildOutgoingRelations(entityId, relations) {
    return relations
        .filter(r => r.fromId === entityId)
        .map(r => {
        const entry = { to: r.toId, kind: r.kind };
        if (r.targetGraph)
            entry.graph = r.targetGraph;
        return entry;
    });
}
/** Append a 'created' event + write content.md + regenerate note.md snapshot. */
function mirrorNoteCreate(notesDir, noteId, attrs, relations) {
    try {
        const safeId = sanitizeEntityId(noteId);
        if (!safeId) {
            log.warn('rejected invalid entity ID');
            return;
        }
        const entityDir = path.join(notesDir, safeId);
        fs.mkdirSync(entityDir, { recursive: true });
        const eventsPath = path.join(entityDir, 'events.jsonl');
        if (!fs.existsSync(eventsPath)) {
            const event = {
                op: 'created',
                id: noteId,
                title: attrs.title,
                tags: attrs.tags,
                createdAt: attrs.createdAt,
            };
            if (attrs.createdBy)
                event.createdBy = attrs.createdBy;
            (0, events_log_1.appendEvent)(eventsPath, event);
        }
        atomicWriteFileSync(path.join(entityDir, 'content.md'), attrs.content, 'utf-8');
        _regenerateNoteSnapshot(notesDir, noteId, attrs, relations);
        (0, events_log_1.ensureGitignore)(notesDir, '*/note.md');
        (0, events_log_1.ensureGitattributes)(notesDir);
    }
    catch (err) {
        log.error({ err, noteId: sanitizeForLog(noteId) }, 'failed to mirror note create');
    }
}
/** Append an 'update' event + (if content changed) write content.md + regenerate note.md. */
function mirrorNoteUpdate(notesDir, noteId, patch, attrs, relations) {
    try {
        const safeId = sanitizeEntityId(noteId);
        if (!safeId) {
            log.warn('rejected invalid entity ID');
            return;
        }
        const entityDir = path.join(notesDir, safeId);
        fs.mkdirSync(entityDir, { recursive: true });
        const eventsPath = path.join(entityDir, 'events.jsonl');
        const delta = { op: 'update' };
        if (patch.title !== undefined)
            delta.title = patch.title;
        if (patch.tags !== undefined)
            delta.tags = patch.tags;
        if (patch.by !== undefined)
            delta.by = patch.by;
        else if (attrs.updatedBy)
            delta.by = attrs.updatedBy;
        if (Object.keys(delta).length > 1)
            (0, events_log_1.appendEvent)(eventsPath, delta);
        if (patch.content !== undefined) {
            atomicWriteFileSync(path.join(entityDir, 'content.md'), patch.content, 'utf-8');
        }
        _regenerateNoteSnapshot(notesDir, noteId, attrs, relations);
    }
    catch (err) {
        log.error({ err, noteId: sanitizeForLog(noteId) }, 'failed to mirror note update');
    }
}
function _regenerateNoteSnapshot(notesDir, noteId, attrs, relations) {
    const outgoing = buildOutgoingRelations(noteId, relations);
    const fm = {
        id: noteId,
        tags: attrs.tags,
        createdAt: tsToIso(attrs.createdAt),
        updatedAt: tsToIso(attrs.updatedAt),
        version: attrs.version,
    };
    if (attrs.createdBy)
        fm.createdBy = attrs.createdBy;
    if (attrs.updatedBy)
        fm.updatedBy = attrs.updatedBy;
    if (outgoing.length > 0)
        fm.relations = outgoing;
    const safeId = sanitizeEntityId(noteId);
    if (!safeId)
        return;
    const body = `# ${attrs.title}\n\n${attrs.content}`;
    const entityDir = path.join(notesDir, safeId);
    fs.mkdirSync(entityDir, { recursive: true });
    atomicWriteFileSync(path.join(entityDir, 'note.md'), (0, frontmatter_1.serializeMarkdown)(fm, body));
}
/** Append a 'created' event + write description.md + regenerate task.md snapshot. */
function mirrorTaskCreate(tasksDir, taskId, attrs, relations) {
    try {
        const safeId = sanitizeEntityId(taskId);
        if (!safeId) {
            log.warn('rejected invalid entity ID');
            return;
        }
        const entityDir = path.join(tasksDir, safeId);
        fs.mkdirSync(entityDir, { recursive: true });
        const eventsPath = path.join(entityDir, 'events.jsonl');
        if (!fs.existsSync(eventsPath)) {
            const event = {
                op: 'created',
                id: taskId,
                title: attrs.title,
                status: attrs.status,
                priority: attrs.priority,
                tags: attrs.tags,
                dueDate: attrs.dueDate,
                estimate: attrs.estimate,
                completedAt: attrs.completedAt,
                createdAt: attrs.createdAt,
            };
            if (attrs.createdBy)
                event.createdBy = attrs.createdBy;
            (0, events_log_1.appendEvent)(eventsPath, event);
        }
        atomicWriteFileSync(path.join(entityDir, 'description.md'), attrs.description, 'utf-8');
        _regenerateTaskSnapshot(tasksDir, taskId, attrs, relations);
        (0, events_log_1.ensureGitignore)(tasksDir, '*/task.md');
        (0, events_log_1.ensureGitattributes)(tasksDir);
    }
    catch (err) {
        log.error({ err, taskId: sanitizeForLog(taskId) }, 'failed to mirror task create');
    }
}
/** Append an 'update' event + (if description changed) write description.md + regenerate task.md. */
function mirrorTaskUpdate(tasksDir, taskId, patch, attrs, relations) {
    try {
        const safeId = sanitizeEntityId(taskId);
        if (!safeId) {
            log.warn('rejected invalid entity ID');
            return;
        }
        const entityDir = path.join(tasksDir, safeId);
        fs.mkdirSync(entityDir, { recursive: true });
        const eventsPath = path.join(entityDir, 'events.jsonl');
        const delta = { op: 'update' };
        if (patch.title !== undefined)
            delta.title = patch.title;
        if (patch.status !== undefined)
            delta.status = patch.status;
        if (patch.priority !== undefined)
            delta.priority = patch.priority;
        if (patch.tags !== undefined)
            delta.tags = patch.tags;
        if ('dueDate' in patch)
            delta.dueDate = patch.dueDate;
        if ('estimate' in patch)
            delta.estimate = patch.estimate;
        if ('completedAt' in patch)
            delta.completedAt = patch.completedAt;
        if (patch.by !== undefined)
            delta.by = patch.by;
        else if (attrs.updatedBy)
            delta.by = attrs.updatedBy;
        if (Object.keys(delta).length > 1)
            (0, events_log_1.appendEvent)(eventsPath, delta);
        if (patch.description !== undefined) {
            atomicWriteFileSync(path.join(entityDir, 'description.md'), patch.description, 'utf-8');
        }
        _regenerateTaskSnapshot(tasksDir, taskId, attrs, relations);
    }
    catch (err) {
        log.error({ err, taskId: sanitizeForLog(taskId) }, 'failed to mirror task update');
    }
}
function _regenerateTaskSnapshot(tasksDir, taskId, attrs, relations) {
    const outgoing = buildOutgoingRelations(taskId, relations);
    const fm = {
        id: taskId,
        status: attrs.status,
        priority: attrs.priority,
        order: attrs.order ?? 0,
        tags: attrs.tags,
        assignee: attrs.assignee ?? null,
        dueDate: tsToIso(attrs.dueDate),
        estimate: attrs.estimate,
        completedAt: tsToIso(attrs.completedAt),
        createdAt: tsToIso(attrs.createdAt),
        updatedAt: tsToIso(attrs.updatedAt),
        version: attrs.version,
    };
    if (attrs.createdBy)
        fm.createdBy = attrs.createdBy;
    if (attrs.updatedBy)
        fm.updatedBy = attrs.updatedBy;
    if (outgoing.length > 0)
        fm.relations = outgoing;
    const safeId = sanitizeEntityId(taskId);
    if (!safeId)
        return;
    const body = `# ${attrs.title}\n\n${attrs.description}`;
    const entityDir = path.join(tasksDir, safeId);
    fs.mkdirSync(entityDir, { recursive: true });
    atomicWriteFileSync(path.join(entityDir, 'task.md'), (0, frontmatter_1.serializeMarkdown)(fm, body));
}
/** Append a 'created' event + write description.md + regenerate skill.md snapshot. */
function mirrorSkillCreate(skillsDir, skillId, attrs, relations) {
    try {
        const safeId = sanitizeEntityId(skillId);
        if (!safeId) {
            log.warn('rejected invalid entity ID');
            return;
        }
        const entityDir = path.join(skillsDir, safeId);
        fs.mkdirSync(entityDir, { recursive: true });
        const eventsPath = path.join(entityDir, 'events.jsonl');
        if (!fs.existsSync(eventsPath)) {
            const event = {
                op: 'created',
                id: skillId,
                title: attrs.title,
                tags: attrs.tags,
                steps: attrs.steps,
                triggers: attrs.triggers,
                inputHints: attrs.inputHints,
                filePatterns: attrs.filePatterns,
                source: attrs.source,
                confidence: attrs.confidence,
                usageCount: attrs.usageCount,
                lastUsedAt: attrs.lastUsedAt,
                createdAt: attrs.createdAt,
            };
            if (attrs.createdBy)
                event.createdBy = attrs.createdBy;
            (0, events_log_1.appendEvent)(eventsPath, event);
        }
        atomicWriteFileSync(path.join(entityDir, 'description.md'), attrs.description, 'utf-8');
        _regenerateSkillSnapshot(skillsDir, skillId, attrs, relations);
        (0, events_log_1.ensureGitignore)(skillsDir, '*/skill.md');
        (0, events_log_1.ensureGitattributes)(skillsDir);
    }
    catch (err) {
        log.error({ err, skillId: sanitizeForLog(skillId) }, 'failed to mirror skill create');
    }
}
/** Append an 'update' event + (if description changed) write description.md + regenerate skill.md. */
function mirrorSkillUpdate(skillsDir, skillId, patch, attrs, relations) {
    try {
        const safeId = sanitizeEntityId(skillId);
        if (!safeId) {
            log.warn('rejected invalid entity ID');
            return;
        }
        const entityDir = path.join(skillsDir, safeId);
        fs.mkdirSync(entityDir, { recursive: true });
        const eventsPath = path.join(entityDir, 'events.jsonl');
        const delta = { op: 'update' };
        if (patch.title !== undefined)
            delta.title = patch.title;
        if (patch.tags !== undefined)
            delta.tags = patch.tags;
        if (patch.steps !== undefined)
            delta.steps = patch.steps;
        if (patch.triggers !== undefined)
            delta.triggers = patch.triggers;
        if (patch.inputHints !== undefined)
            delta.inputHints = patch.inputHints;
        if (patch.filePatterns !== undefined)
            delta.filePatterns = patch.filePatterns;
        if (patch.source !== undefined)
            delta.source = patch.source;
        if (patch.confidence !== undefined)
            delta.confidence = patch.confidence;
        if (patch.usageCount !== undefined)
            delta.usageCount = patch.usageCount;
        if ('lastUsedAt' in patch)
            delta.lastUsedAt = patch.lastUsedAt;
        if (patch.by !== undefined)
            delta.by = patch.by;
        else if (attrs.updatedBy)
            delta.by = attrs.updatedBy;
        if (Object.keys(delta).length > 1)
            (0, events_log_1.appendEvent)(eventsPath, delta);
        if (patch.description !== undefined) {
            atomicWriteFileSync(path.join(entityDir, 'description.md'), patch.description, 'utf-8');
        }
        _regenerateSkillSnapshot(skillsDir, skillId, attrs, relations);
    }
    catch (err) {
        log.error({ err, skillId: sanitizeForLog(skillId) }, 'failed to mirror skill update');
    }
}
function _regenerateSkillSnapshot(skillsDir, skillId, attrs, relations) {
    const outgoing = buildOutgoingRelations(skillId, relations);
    const fm = {
        id: skillId,
        source: attrs.source,
        confidence: attrs.confidence,
        triggers: attrs.triggers,
        inputHints: attrs.inputHints,
        filePatterns: attrs.filePatterns,
        tags: attrs.tags,
        createdAt: tsToIso(attrs.createdAt),
        updatedAt: tsToIso(attrs.updatedAt),
        version: attrs.version,
    };
    if (attrs.createdBy)
        fm.createdBy = attrs.createdBy;
    if (attrs.updatedBy)
        fm.updatedBy = attrs.updatedBy;
    if (outgoing.length > 0)
        fm.relations = outgoing;
    const stepsBlock = attrs.steps.length > 0
        ? `\n\n## Steps\n${attrs.steps.map((s, i) => `${i + 1}. ${s}`).join('\n')}`
        : '';
    const safeId = sanitizeEntityId(skillId);
    if (!safeId)
        return;
    const body = `# ${attrs.title}\n\n${attrs.description}${stepsBlock}`;
    const entityDir = path.join(skillsDir, safeId);
    fs.mkdirSync(entityDir, { recursive: true });
    atomicWriteFileSync(path.join(entityDir, 'skill.md'), (0, frontmatter_1.serializeMarkdown)(fm, body));
}
// ---------------------------------------------------------------------------
// Relation + attachment event append helpers
// ---------------------------------------------------------------------------
/** Append a relation add/remove event and regenerate snapshot. */
function mirrorNoteRelation(notesDir, noteId, action, kind, to, attrs, relations, graph) {
    try {
        const safeId = sanitizeEntityId(noteId);
        if (!safeId)
            return;
        const entityDir = path.join(notesDir, safeId);
        const eventsPath = path.join(entityDir, 'events.jsonl');
        const event = { op: 'relation', action, kind, to };
        if (graph)
            event.graph = graph;
        (0, events_log_1.appendEvent)(eventsPath, event);
        _regenerateNoteSnapshot(notesDir, noteId, attrs, relations);
    }
    catch (err) {
        log.error({ err, noteId: sanitizeForLog(noteId) }, 'failed to mirror note relation');
    }
}
function mirrorTaskRelation(tasksDir, taskId, action, kind, to, attrs, relations, graph) {
    try {
        const safeId = sanitizeEntityId(taskId);
        if (!safeId)
            return;
        const entityDir = path.join(tasksDir, safeId);
        const eventsPath = path.join(entityDir, 'events.jsonl');
        const event = { op: 'relation', action, kind, to };
        if (graph)
            event.graph = graph;
        (0, events_log_1.appendEvent)(eventsPath, event);
        _regenerateTaskSnapshot(tasksDir, taskId, attrs, relations);
    }
    catch (err) {
        log.error({ err, taskId: sanitizeForLog(taskId) }, 'failed to mirror task relation');
    }
}
function mirrorSkillRelation(skillsDir, skillId, action, kind, to, attrs, relations, graph) {
    try {
        const safeId = sanitizeEntityId(skillId);
        if (!safeId)
            return;
        const entityDir = path.join(skillsDir, safeId);
        const eventsPath = path.join(entityDir, 'events.jsonl');
        const event = { op: 'relation', action, kind, to };
        if (graph)
            event.graph = graph;
        (0, events_log_1.appendEvent)(eventsPath, event);
        _regenerateSkillSnapshot(skillsDir, skillId, attrs, relations);
    }
    catch (err) {
        log.error({ err, skillId: sanitizeForLog(skillId) }, 'failed to mirror skill relation');
    }
}
/** Append an attachment add/remove event. */
function mirrorAttachmentEvent(entityDir, action, file) {
    try {
        const eventsPath = path.join(entityDir, 'events.jsonl');
        (0, events_log_1.appendEvent)(eventsPath, { op: 'attachment', action, file });
    }
    catch (err) {
        log.error({ err }, 'failed to mirror attachment event');
    }
}
// ---------------------------------------------------------------------------
// Delete
// ---------------------------------------------------------------------------
/** Delete the entire mirror directory for a note, task or skill (including attachments). */
function deleteMirrorDir(dir, id) {
    const safeId = sanitizeEntityId(id);
    if (!safeId) {
        log.warn('rejected invalid entity ID');
        return;
    }
    try {
        fs.rmSync(path.join(dir, safeId), { recursive: true, force: true });
    }
    catch (err) {
        if (err.code !== 'ENOENT') {
            log.error({ err, id: sanitizeForLog(id) }, 'failed to delete mirror dir');
        }
    }
}
// ---------------------------------------------------------------------------
// Attachment file helpers (paths now go through attachments/ subdir)
// ---------------------------------------------------------------------------
/** Sanitize an entity ID: extract basename, strip null bytes and path traversal. */
function sanitizeEntityId(id) {
    const base = path.basename(id.replace(/\0/g, '').replace(/\\/g, '/')).trim();
    if (base === '.' || base === '..')
        return '';
    return base;
}
/** Sanitize a filename: extract basename, strip null bytes and path traversal. */
function sanitizeFilename(name) {
    // Normalize backslashes to forward slashes (path.basename on Unix doesn't treat \ as separator)
    const base = path.basename(name.replace(/\0/g, '').replace(/\\/g, '/')).trim();
    // Reject pure traversal names
    if (base === '.' || base === '..')
        return '';
    return base;
}
/** Write an attachment file to the entity's attachments/ subdirectory. */
function writeAttachment(baseDir, entityId, filename, data) {
    const safeEntityId = sanitizeEntityId(entityId);
    if (!safeEntityId)
        throw new Error('Entity ID is empty after sanitization');
    const safe = sanitizeFilename(filename);
    if (!safe)
        throw new Error('Attachment filename is empty after sanitization');
    const attachmentsDir = path.join(baseDir, safeEntityId, 'attachments');
    fs.mkdirSync(attachmentsDir, { recursive: true });
    fs.writeFileSync(path.join(attachmentsDir, safe), data);
}
/** Delete an attachment file from attachments/ subdir. Returns true if it existed. */
function deleteAttachment(baseDir, entityId, filename) {
    const safeEntityId = sanitizeEntityId(entityId);
    if (!safeEntityId)
        return false;
    const filePath = path.join(baseDir, safeEntityId, 'attachments', sanitizeFilename(filename));
    try {
        fs.unlinkSync(filePath);
        return true;
    }
    catch (err) {
        if (err.code !== 'ENOENT') {
            log.error({ err, filename: sanitizeForLog(filename) }, 'failed to delete attachment');
        }
        return false;
    }
}
/** Get the absolute path of an attachment in attachments/ subdir, or null if not found. */
function getAttachmentPath(baseDir, entityId, filename) {
    const safeEntityId = sanitizeEntityId(entityId);
    if (!safeEntityId)
        return null;
    const filePath = path.join(baseDir, safeEntityId, 'attachments', sanitizeFilename(filename));
    return fs.existsSync(filePath) ? filePath : null;
}
