/**
 * Git History Tools for the Studiograph Agent
 *
 * Read tools (gated on canAccessRepo):
 *   - list_file_history    — commits that touched one entity's file
 *   - list_folder_history  — recent commits across a whole repo
 *   - list_recent_deletions — entities deleted within the last N days
 *   - read_file_at_commit  — file content at a past commit hash
 *   - get_entity_diff      — unified diff between two commits for one entity
 *
 * Write tool (gated on canWriteRepo, preview-then-confirm):
 *   - restore_file         — restore one entity to its content at a commit
 *
 * Single-entity scope only. `revert_commit` (multi-entity rollback)
 * is intentionally deferred until real demand surfaces — when added,
 * it will live in this same file with the same gating model.
 *
 * The system prompt restraint rule applies to the write tool:
 * never restore without explicit user request, and always surface the
 * preview payload before re-invoking with confirm: true.
 */
import { relative } from 'path';
import { Type as T } from 'typebox';
import { PathConflictError } from '../../core/graph.js';
import * as access from '../../services/access-control.js';
import { wrapToolResult } from './graph-tools.js';
function canAccessRepo(repoName, workspaceManager, getCurrentUser, authService) {
    const user = getCurrentUser();
    if (!user)
        return true;
    if (!authService)
        return true;
    return access.hasRepoAccess(user, repoName, workspaceManager, authService);
}
function canWriteRepo(repoName, workspaceManager, getCurrentUser, authService) {
    const user = getCurrentUser();
    if (!user)
        return true;
    if (!authService)
        return true;
    return access.canWriteRepo(user, repoName, workspaceManager, authService);
}
export function createHistoryTools(workspaceManager, gitUser, onMutation, getCurrentUser, authService, 
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_onAgentActivity) {
    const getUser = getCurrentUser ?? (() => null);
    const authSvc = authService ?? null;
    function effectiveGitUser() {
        const user = getUser();
        if (user?.handle) {
            return {
                id: user.handle,
                name: user.displayName ?? user.handle,
                email: `${user.handle}@studiograph.local`,
            };
        }
        return gitUser;
    }
    /** Resolve a repo for read access — returns the graph or an error envelope. */
    function resolveForRead(repoName) {
        if (!canAccessRepo(repoName, workspaceManager, getUser, authSvc)) {
            return { error: `Folder "${repoName}" not found` };
        }
        try {
            return { graph: workspaceManager.getGraph(repoName) };
        }
        catch (err) {
            return { error: err?.message ?? `Could not open folder "${repoName}"` };
        }
    }
    /** Resolve a repo for write access — same as read but also checks the
     *  caller has editor-or-admin role. Used by `restore_file`. */
    function resolveForWrite(repoName) {
        if (!canAccessRepo(repoName, workspaceManager, getUser, authSvc)) {
            return { error: `Folder "${repoName}" not found` };
        }
        if (!canWriteRepo(repoName, workspaceManager, getUser, authSvc)) {
            return { error: `You don't have permission to write to "${repoName}". Editor or admin role required.` };
        }
        try {
            return { graph: workspaceManager.getGraph(repoName) };
        }
        catch (err) {
            return { error: err?.message ?? `Could not open folder "${repoName}"` };
        }
    }
    /**
     * Resolve an entity's repo-relative file path. Tries the live index
     * first; for deleted entities falls back to the most recent deletion
     * record whose filename matches `{entityId}.md`. Returns null when
     * the entity has never existed (or existed but is older than the
     * deletion-scan window).
     */
    function resolveEntityPath(graph, entityType, entityId) {
        try {
            const entity = graph.get(entityType, entityId);
            return relative(graph.getRepoPath(), entity.path);
        }
        catch {
            const gitService = graph.getGitService();
            if (!gitService)
                return null;
            const idLower = `${entityId}.md`.toLowerCase();
            const deletions = gitService.listRecentDeletions(30);
            const match = deletions.find((d) => d.filePath.toLowerCase().endsWith(idLower));
            return match ? match.filePath : null;
        }
    }
    /**
     * Map a deleted file path back to an `(entityType, entityId)` pair.
     * Heuristic: read the file content at `lastVersionCommit` and parse
     * its frontmatter; fall back to the legacy plural-folder convention
     * (e.g. `meetings/foo.md` → `meeting`). Returns null when neither
     * source identifies a known entity type.
     */
    function deletionToEntityCoords(graph, filePath, lastVersionCommit) {
        const idMatch = filePath.match(/([^/]+)\.md$/i);
        if (!idMatch)
            return null;
        const entityId = idMatch[1];
        const gitService = graph.getGitService();
        if (!gitService)
            return null;
        const content = gitService.getFileAtCommit(filePath, lastVersionCommit);
        if (!content)
            return null;
        try {
            // Use the graph's markdown service indirectly via getEntityAtCommit's
            // path: we already have content, so a quick frontmatter sniff is
            // sufficient. matter() is heavy; a regex on the frontmatter block
            // is enough for the type field.
            const fmMatch = content.match(/^---\s*\n([\s\S]*?)\n---/);
            if (fmMatch) {
                const typeMatch = fmMatch[1].match(/^entity_type:\s*['"]?([\w-]+)['"]?\s*$/m);
                if (typeMatch)
                    return { entityType: typeMatch[1], entityId };
            }
            // Legacy fallback: top-level plural folder name → singular.
            const segments = filePath.split('/');
            if (segments.length >= 2) {
                // Lazy import to avoid circular dep — PLURAL_TO_SINGULAR lives
                // in validation.ts which the agent layer already pulls in.
                const { PLURAL_TO_SINGULAR } = require('../../core/validation.js');
                const inferred = PLURAL_TO_SINGULAR[segments[0]];
                if (inferred)
                    return { entityType: inferred, entityId };
            }
            return null;
        }
        catch {
            return null;
        }
    }
    return [
        // ── list_file_history ────────────────────────────────────────────
        {
            name: 'list_file_history',
            label: 'List File History',
            description: 'List the commits that touched a single entry, newest first. Use this when the user asks "what changed on this", "when did I last edit this", or before restoring a file to find the right version. Works for both existing and recently-deleted entries (deletion lookup window: 30 days).',
            parameters: T.Object({
                repo: T.String({ description: 'Folder name containing the entry' }),
                entityType: T.String({ description: 'Entity type (e.g. "note", "task", "event")' }),
                entityId: T.String({ description: 'Entry ID' }),
                limit: T.Optional(T.Number({ description: 'Max commits to return (default 20, max 100)' })),
            }),
            execute: async (_toolCallId, params) => {
                try {
                    const resolved = resolveForRead(params.repo);
                    if ('error' in resolved)
                        return wrapToolResult({ success: false, error: resolved.error });
                    const gitService = resolved.graph.getGitService();
                    if (!gitService)
                        return wrapToolResult({ success: false, error: 'Folder is not git-backed' });
                    const entityPath = resolveEntityPath(resolved.graph, params.entityType, params.entityId);
                    if (!entityPath) {
                        return wrapToolResult({ success: false, error: `Entry ${params.entityType}/${params.entityId} not found in "${params.repo}" (and no recent deletion matches).` });
                    }
                    const limit = Math.min(100, Math.max(1, params.limit ?? 20));
                    const commits = gitService.getFileHistory(entityPath, limit).map((c) => ({
                        hash: c.hash.slice(0, 8),
                        fullHash: c.hash,
                        author: c.author,
                        timestamp: c.timestamp,
                        message: c.message,
                    }));
                    return wrapToolResult({
                        success: true,
                        repo: params.repo,
                        entityType: params.entityType,
                        entityId: params.entityId,
                        commits,
                        message: commits.length === 0
                            ? `No commits found for ${params.entityType}/${params.entityId}.`
                            : `Found ${commits.length} commit${commits.length === 1 ? '' : 's'}.`,
                    });
                }
                catch (err) {
                    return wrapToolResult({ success: false, error: err instanceof Error ? err.message : 'Unknown error' });
                }
            },
        },
        // ── list_folder_history ──────────────────────────────────────────
        {
            name: 'list_folder_history',
            label: 'List Folder History',
            description: 'List recent commits across an entire folder, newest first. Use when the user asks "what changed in this folder", "what did we ship yesterday", or wants a high-level audit trail. For per-entry history, prefer `list_file_history`.',
            parameters: T.Object({
                repo: T.String({ description: 'Folder name to inspect' }),
                limit: T.Optional(T.Number({ description: 'Max commits to return (default 30, max 200)' })),
            }),
            execute: async (_toolCallId, params) => {
                try {
                    const resolved = resolveForRead(params.repo);
                    if ('error' in resolved)
                        return wrapToolResult({ success: false, error: resolved.error });
                    const gitService = resolved.graph.getGitService();
                    if (!gitService)
                        return wrapToolResult({ success: false, error: 'Folder is not git-backed' });
                    const limit = Math.min(200, Math.max(1, params.limit ?? 30));
                    const commits = gitService.getHistory(limit).map((c) => ({
                        hash: c.hash.slice(0, 8),
                        fullHash: c.hash,
                        author: c.author,
                        timestamp: c.timestamp,
                        message: c.message,
                    }));
                    return wrapToolResult({
                        success: true,
                        repo: params.repo,
                        commits,
                        message: commits.length === 0
                            ? `No commits in folder "${params.repo}".`
                            : `Found ${commits.length} commit${commits.length === 1 ? '' : 's'}.`,
                    });
                }
                catch (err) {
                    return wrapToolResult({ success: false, error: err instanceof Error ? err.message : 'Unknown error' });
                }
            },
        },
        // ── list_recent_deletions ────────────────────────────────────────
        {
            name: 'list_recent_deletions',
            label: 'List Recent Deletions',
            description: 'List entries deleted within the last N days, newest deletion first. The discovery primitive for "what did I delete" or "restore that thing I deleted" workflows — call this BEFORE `restore_file` to find the entry coordinates and the commit hash to restore from.',
            parameters: T.Object({
                repo: T.String({ description: 'Folder name to inspect' }),
                withinDays: T.Optional(T.Number({ description: 'Lookback window (default 7, max 90)' })),
            }),
            execute: async (_toolCallId, params) => {
                try {
                    const resolved = resolveForRead(params.repo);
                    if ('error' in resolved)
                        return wrapToolResult({ success: false, error: resolved.error });
                    const gitService = resolved.graph.getGitService();
                    if (!gitService)
                        return wrapToolResult({ success: false, error: 'Folder is not git-backed' });
                    const days = Math.min(90, Math.max(1, params.withinDays ?? 7));
                    const raw = gitService.listRecentDeletions(days);
                    const deletions = raw
                        .map((d) => {
                        const coords = deletionToEntityCoords(resolved.graph, d.filePath, d.lastVersionCommit);
                        if (!coords)
                            return null;
                        return {
                            entityType: coords.entityType,
                            entityId: coords.entityId,
                            deletedAt: d.deletedAt,
                            deletedBy: d.deletedBy.name,
                            deletedInCommit: d.deletedAtCommit.slice(0, 8),
                            lastVersionCommit: d.lastVersionCommit,
                            commitMessage: d.commitMessage,
                            path: d.filePath,
                        };
                    })
                        .filter((x) => x !== null);
                    return wrapToolResult({
                        success: true,
                        repo: params.repo,
                        withinDays: days,
                        deletions,
                        message: deletions.length === 0
                            ? `No entries deleted in "${params.repo}" within the last ${days} day${days === 1 ? '' : 's'}.`
                            : `Found ${deletions.length} deleted ${deletions.length === 1 ? 'entry' : 'entries'}.`,
                    });
                }
                catch (err) {
                    return wrapToolResult({ success: false, error: err instanceof Error ? err.message : 'Unknown error' });
                }
            },
        },
        // ── read_file_at_commit ──────────────────────────────────────────
        {
            name: 'read_file_at_commit',
            label: 'Read File at Commit',
            description: 'Read the full content (frontmatter + body) of an entry at a specific commit. Use to preview what `restore_file` would put back, or to compare past content before deciding to restore.',
            parameters: T.Object({
                repo: T.String({ description: 'Folder name' }),
                entityType: T.String({ description: 'Entity type' }),
                entityId: T.String({ description: 'Entry ID' }),
                commitHash: T.String({ description: 'Commit hash (short or full)' }),
            }),
            execute: async (_toolCallId, params) => {
                try {
                    const resolved = resolveForRead(params.repo);
                    if ('error' in resolved)
                        return wrapToolResult({ success: false, error: resolved.error });
                    const found = resolved.graph.getEntityAtCommit(params.entityType, params.entityId, params.commitHash);
                    if (!found) {
                        return wrapToolResult({ success: false, error: `Entry ${params.entityType}/${params.entityId} not found at commit ${params.commitHash}` });
                    }
                    return wrapToolResult({
                        success: true,
                        repo: params.repo,
                        entityType: params.entityType,
                        entityId: params.entityId,
                        commitHash: params.commitHash,
                        path: found.path,
                        content: found.content,
                    });
                }
                catch (err) {
                    return wrapToolResult({ success: false, error: err instanceof Error ? err.message : 'Unknown error' });
                }
            },
        },
        // ── get_entity_diff ──────────────────────────────────────────────
        {
            name: 'get_entity_diff',
            label: 'Get Entry Diff',
            description: 'Unified diff for one entry between two commits (or fromCommit → HEAD). Use to show the user what changed before they decide to restore or revert.',
            parameters: T.Object({
                repo: T.String({ description: 'Folder name' }),
                entityType: T.String({ description: 'Entity type' }),
                entityId: T.String({ description: 'Entry ID' }),
                fromCommit: T.String({ description: 'From commit hash (the older side of the diff)' }),
                toCommit: T.Optional(T.String({ description: 'To commit hash. Omit to diff against HEAD.' })),
            }),
            execute: async (_toolCallId, params) => {
                try {
                    const resolved = resolveForRead(params.repo);
                    if ('error' in resolved)
                        return wrapToolResult({ success: false, error: resolved.error });
                    const gitService = resolved.graph.getGitService();
                    if (!gitService)
                        return wrapToolResult({ success: false, error: 'Folder is not git-backed' });
                    const entityPath = resolveEntityPath(resolved.graph, params.entityType, params.entityId);
                    if (!entityPath) {
                        return wrapToolResult({ success: false, error: `Entry ${params.entityType}/${params.entityId} not found in "${params.repo}".` });
                    }
                    const to = params.toCommit ?? 'HEAD';
                    let diff;
                    try {
                        diff = gitService.getDiff(params.fromCommit, to, entityPath);
                    }
                    catch (err) {
                        return wrapToolResult({ success: false, error: err?.message ?? 'git diff failed' });
                    }
                    return wrapToolResult({
                        success: true,
                        repo: params.repo,
                        entityType: params.entityType,
                        entityId: params.entityId,
                        fromCommit: params.fromCommit,
                        toCommit: to,
                        path: entityPath,
                        diff,
                    });
                }
                catch (err) {
                    return wrapToolResult({ success: false, error: err instanceof Error ? err.message : 'Unknown error' });
                }
            },
        },
        // ── restore_file ─────────────────────────────────────────────────
        {
            name: 'restore_file',
            label: 'Restore File',
            description: 'Restore an entry to its content at a specific commit. Recreates the entry if it was deleted, overwrites if it exists. ALWAYS preview first — call without `confirm: true` to receive the preview payload, surface it to the user, and only re-invoke with `confirm: true` after explicit approval. Single-entity scope; multi-entity rollback is not yet supported.',
            parameters: T.Object({
                repo: T.String({ description: 'Folder name' }),
                entityType: T.String({ description: 'Entity type' }),
                entityId: T.String({ description: 'Entry ID' }),
                commitHash: T.String({ description: 'Commit hash to restore from (short or full). For deleted entries, use lastVersionCommit from list_recent_deletions.' }),
                confirm: T.Optional(T.Boolean({ description: 'Set to true ONLY after the user has approved the preview. Defaults to false.' })),
            }),
            execute: async (_toolCallId, params) => {
                try {
                    const resolved = resolveForWrite(params.repo);
                    if ('error' in resolved)
                        return wrapToolResult({ success: false, error: resolved.error });
                    // Preview path: report what the restore WOULD do, refuse to execute.
                    if (params.confirm !== true) {
                        const found = resolved.graph.getEntityAtCommit(params.entityType, params.entityId, params.commitHash);
                        if (!found) {
                            return wrapToolResult({ success: false, error: `Entry ${params.entityType}/${params.entityId} not found at commit ${params.commitHash}` });
                        }
                        // Detect overwrite vs recreate by checking the live index.
                        let action = 'recreate';
                        try {
                            resolved.graph.get(params.entityType, params.entityId);
                            action = 'overwrite';
                        }
                        catch { /* gone today → recreate */ }
                        const sizeBytes = Buffer.byteLength(found.content, 'utf-8');
                        const shortHash = params.commitHash.length > 8 ? params.commitHash.slice(0, 8) : params.commitHash;
                        return wrapToolResult({
                            success: false,
                            code: 'PREVIEW_REQUIRED',
                            preview: {
                                action,
                                targetPath: found.path,
                                sizeBytes,
                                commitHash: shortHash,
                            },
                            message: `Will ${action} ${params.entityType}/${params.entityId} at ${params.repo}/${found.path} from commit ${shortHash} (${sizeBytes} bytes). Confirm with the user before re-invoking with confirm: true.`,
                        });
                    }
                    // Execute path.
                    const actor = effectiveGitUser();
                    let result;
                    try {
                        result = await resolved.graph.restoreFile(params.entityType, params.entityId, params.commitHash, actor);
                    }
                    catch (err) {
                        if (err instanceof PathConflictError) {
                            return wrapToolResult({
                                success: false,
                                code: 'PATH_CONFLICT',
                                conflict: { path: err.path, occupiedBy: err.occupiedBy },
                                message: `Cannot restore — ${err.path} is occupied by ${err.occupiedBy.entityType}/${err.occupiedBy.entityId}. Move or delete the occupant first.`,
                            });
                        }
                        throw err;
                    }
                    onMutation?.({
                        action: result.action === 'recreate' ? 'created' : 'updated',
                        repo: params.repo,
                        entityType: params.entityType,
                        entityId: params.entityId,
                    });
                    const shortHash = params.commitHash.length > 8 ? params.commitHash.slice(0, 8) : params.commitHash;
                    return wrapToolResult({
                        success: true,
                        repo: params.repo,
                        entityType: params.entityType,
                        entityId: params.entityId,
                        action: result.action,
                        path: result.path,
                        commitHash: shortHash,
                        message: `Restored ${params.entityType}/${params.entityId} from ${shortHash} (${result.action}). Auto-committed.`,
                    });
                }
                catch (err) {
                    return wrapToolResult({ success: false, error: err instanceof Error ? err.message : 'Unknown error' });
                }
            },
        },
    ];
}
//# sourceMappingURL=history-tools.js.map