export const DEFAULT_LOCK_TTL_MS = 30 * 60 * 1000;
// ---------------------------------------------------------------------------
// Internal helpers
// ---------------------------------------------------------------------------
export function isLockExpired(lock) {
    if (!lock.expiresAt) {
        return false;
    }
    return new Date(lock.expiresAt) < new Date();
}
export function pruneExpiredLocks(state) {
    const activeLocks = state.locks.filter((l) => !isLockExpired(l));
    for (const session of state.sessions) {
        session.lockedResources = session.lockedResources.filter((lr) => !isLockExpired(lr));
    }
    return { ...state, locks: activeLocks };
}
function lockTtlMs(lockTtlMinutes) {
    return lockTtlMinutes ? lockTtlMinutes * 60 * 1000 : DEFAULT_LOCK_TTL_MS;
}
function resourceType(resourcePath, specId, resourceId) {
    if (specId && resourceId === specId) {
        return 'spec';
    }
    if (resourcePath?.endsWith('.json') || resourcePath?.includes('config')) {
        return 'config';
    }
    if (resourcePath?.includes('knowledge')) {
        return 'knowledge';
    }
    return 'file';
}
// ---------------------------------------------------------------------------
// Action: heartbeat
// ---------------------------------------------------------------------------
export function handleHeartbeatState(state, sessionId, lockTtlMinutes) {
    const session = state.sessions.find((s) => s.sessionId === sessionId);
    if (!session) {
        return { error: `Session '${sessionId}' not found. Register first.` };
    }
    const now = new Date().toISOString();
    session.lastActivity = now;
    session.status = 'active';
    // Auto-renew locks held by this session
    const ttl = lockTtlMs(lockTtlMinutes);
    for (const lock of state.locks) {
        if (lock.lockedBy === sessionId) {
            lock.expiresAt = new Date(Date.now() + ttl).toISOString();
        }
    }
    return {
        state,
        result: {
            action: 'heartbeat',
            sessionId,
            message: `Heartbeat received for session '${sessionId}'`,
        },
    };
}
// ---------------------------------------------------------------------------
// Action: lock
// ---------------------------------------------------------------------------
export function handleLockState(state, sessionId, resourcePath, specId, lockTtlMinutes, fileOwnership) {
    const resourceId = resourcePath ?? specId;
    if (!resourceId) {
        return { error: 'resourcePath or specId is required for lock action' };
    }
    const session = state.sessions.find((s) => s.sessionId === sessionId);
    if (!session) {
        return { error: `Session '${sessionId}' not found. Register first.` };
    }
    const existingLock = state.locks.find((l) => l.resourceId === resourceId && !isLockExpired(l));
    if (existingLock && existingLock.lockedBy !== sessionId) {
        return {
            state,
            result: {
                action: 'lock',
                sessionId,
                granted: false,
                message: `Resource '${resourceId}' is already locked by '${existingLock.lockedBy}' until ${existingLock.expiresAt ?? 'indefinitely'}`,
            },
        };
    }
    const ttl = lockTtlMs(lockTtlMinutes);
    if (existingLock?.lockedBy === sessionId) {
        const expiresAt = new Date(Date.now() + ttl).toISOString();
        existingLock.expiresAt = expiresAt;
        return {
            state,
            result: {
                action: 'lock',
                sessionId,
                granted: true,
                message: `Lock on '${resourceId}' extended until ${expiresAt}`,
            },
        };
    }
    const now = new Date();
    const expiresAt = new Date(now.getTime() + ttl).toISOString();
    const type = resourceType(resourcePath, specId, resourceId);
    const lock = {
        type,
        resourceId,
        lockedBy: sessionId,
        lockedAt: now.toISOString(),
        reason: `Locked by session '${sessionId}'`,
        expiresAt,
    };
    state.locks.push(lock);
    session.lockedResources.push(lock);
    session.lastActivity = now.toISOString();
    // Update fileOwnership if provided
    if (fileOwnership?.length) {
        session.fileOwnership = [...(session.fileOwnership ?? []), ...fileOwnership];
    }
    return {
        state,
        result: {
            action: 'lock',
            sessionId,
            granted: true,
            message: `Resource '${resourceId}' locked by '${sessionId}' until ${expiresAt}`,
        },
    };
}
// ---------------------------------------------------------------------------
// Action: unlock
// ---------------------------------------------------------------------------
export function handleUnlockState(state, sessionId, resourcePath, specId) {
    const resourceId = resourcePath ?? specId;
    if (!resourceId) {
        return { error: 'resourcePath or specId is required for unlock action' };
    }
    const lockIdx = state.locks.findIndex((l) => l.resourceId === resourceId && l.lockedBy === sessionId);
    if (lockIdx === -1) {
        return { error: `No lock on '${resourceId}' held by '${sessionId}'` };
    }
    state.locks.splice(lockIdx, 1);
    const session = state.sessions.find((s) => s.sessionId === sessionId);
    if (session) {
        session.lockedResources = session.lockedResources.filter((lr) => lr.resourceId !== resourceId);
        session.lastActivity = new Date().toISOString();
    }
    return {
        state,
        result: {
            action: 'unlock',
            sessionId,
            granted: true,
            message: `Resource '${resourceId}' unlocked by '${sessionId}'`,
        },
    };
}
// ---------------------------------------------------------------------------
// Wrappers that turn errors into ToolResult
// ---------------------------------------------------------------------------
function errResult(msg) {
    return {
        content: [{ type: 'text', text: JSON.stringify({ error: msg }, null, 2) }],
        isError: true,
    };
}
export function toHeartbeatResult(sessionId, state, lockTtlMinutes) {
    if (!sessionId) {
        return { mutated: null, toolResult: errResult('sessionId is required for heartbeat action') };
    }
    const out = handleHeartbeatState(state, sessionId, lockTtlMinutes);
    if ('error' in out) {
        return { mutated: null, toolResult: errResult(out.error) };
    }
    return {
        mutated: out.state,
        toolResult: { content: [{ type: 'text', text: JSON.stringify(out.result, null, 2) }] },
    };
}
export function toLockResult(sessionId, state, resourcePath, specId, lockTtlMinutes, fileOwnership) {
    if (!sessionId) {
        return { mutated: null, toolResult: errResult('sessionId is required for lock action') };
    }
    const out = handleLockState(state, sessionId, resourcePath, specId, lockTtlMinutes, fileOwnership);
    if ('error' in out) {
        return { mutated: null, toolResult: errResult(out.error) };
    }
    return {
        mutated: out.state,
        toolResult: { content: [{ type: 'text', text: JSON.stringify(out.result, null, 2) }] },
    };
}
export function toUnlockResult(sessionId, state, resourcePath, specId) {
    if (!sessionId) {
        return { mutated: null, toolResult: errResult('sessionId is required for unlock action') };
    }
    const out = handleUnlockState(state, sessionId, resourcePath, specId);
    if ('error' in out) {
        return { mutated: null, toolResult: errResult(out.error) };
    }
    return {
        mutated: out.state,
        toolResult: { content: [{ type: 'text', text: JSON.stringify(out.result, null, 2) }] },
    };
}
//# sourceMappingURL=orchestrate-locking.js.map