// packages/core/command-handlers/src/ws-command-handlers.ts
import {
  getAppErrorCause,
  isAppError,
  throwAppError,
} from '@learnitive/base-utils';
import { EDITOR_SAVE_DRAIN_TIMEOUT_MS } from '@learnitive/constants';
import { waitForSaveQueueToDrain } from '@learnitive/service-core';
import { projectDirectoryPath } from '@learnitive/project-storage';
import { toast } from '@learnitive/ui-components';
import { WsDirPath, WsPath } from '@learnitive/ws-path';
import { c, getCtx } from './helper';

import { validateInputPath } from './utils';

function assertDirectoryWsPath(wsPath: string, errorPath = wsPath): WsDirPath {
  const dirPath = WsPath.fromString(wsPath).asDir();

  if (!dirPath || dirPath.isRoot) {
    throwAppError(
      'error::ws-path:invalid-ws-path',
      t.app.errors.wsPath.invalidDirectoryPath,
      {
        invalidPath: errorPath,
      },
    );
  }

  return dirPath;
}

function isUnderDirectory(filePath: WsPath, dirPath: WsDirPath): boolean {
  return (
    filePath.wsName === dirPath.wsName && filePath.path.startsWith(dirPath.path)
  );
}

function basename(path: string): string {
  return path.replace(/\/+$/, '').split('/').at(-1) || path;
}

function normalizeDestinationDirWsPath(destDirWsPath: string): string {
  const separatorIndex = destDirWsPath.indexOf(':');
  if (separatorIndex < 0) {
    return destDirWsPath;
  }

  const wsName = destDirWsPath.slice(0, separatorIndex);
  const path = destDirWsPath.slice(separatorIndex + 1).replace(/^\/+/, '');
  return path ? WsDirPath.fromParts(wsName, path).wsPath : `${wsName}:`;
}

function rethrowRelocationError(
  error: unknown,
  newWsPath: string,
  fileName: string,
  fallbackMessage: string,
): never {
  if (isAppError(error)) {
    if (getAppErrorCause(error)?.name === 'error::file:already-existing') {
      throwAppError(
        'error::file:already-existing',
        t.app.errors.file.alreadyExistsInDest({ fileName }),
        { wsPath: newWsPath },
      );
    }
    throw error;
  }

  toast.error(fallbackMessage);
  throw error;
}

async function assertSourceSavesDrained(
  editorEngine: {
    hasPendingOrFailedSave: (wsPath?: string) => boolean;
    subscribeToSaveStatus: (
      listener: () => void,
      wsPath?: string,
    ) => () => void;
  },
  relocations: readonly { oldWsPath: string; newWsPath: string }[],
  operation: 'move' | 'rename',
): Promise<void> {
  const drainResults = await Promise.all(
    relocations.map(({ oldWsPath }) =>
      waitForSaveQueueToDrain(
        editorEngine,
        EDITOR_SAVE_DRAIN_TIMEOUT_MS,
        oldWsPath,
      ),
    ),
  );
  const blockedIndex = drainResults.findIndex((drained) => !drained);
  const blockedRelocation = relocations[blockedIndex];
  if (blockedRelocation) {
    throwAppError(
      'error::file:invalid-operation',
      t.app.errors.file.relocationBlockedByUnsavedChanges,
      { operation, ...blockedRelocation },
    );
  }
}

export const wsCommandHandlers = [
  c('command::ws:new-file-from-input', ({ navigation }, { inputPath }, key) => {
    const { dispatch } = getCtx(key);
    const exactInputPath = inputPath.trim();
    validateInputPath(exactInputPath);

    const { wsName } = navigation.resolveAtoms();
    if (!wsName) {
      throwAppError(
        'error::workspace:not-opened',
        t.app.errors.workspace.notOpened,
        { wsPath: exactInputPath },
      );
    }

    // Project files preserve the exact user-supplied path. In particular,
    // `main.tex` must never become `main.tex.md`. Markdown remains available
    // when the user explicitly supplies a `.md` filename.
    const wsPath = WsPath.fromParts(wsName, exactInputPath).toString();
    dispatch('command::ws:create-file', { wsPath, navigate: true });
  }),

  c(
    'command::ws:create-file',
    async ({ fileSystem, navigation, projectCollaboration }, { wsPath, navigate }) => {
      const filePath = WsPath.assertFile(wsPath);
      projectCollaboration.assertProjectStructureMutable(filePath.wsName, 'create files');
      const navigationVersion = navigation.captureNavigationVersion();
      try {
        await fileSystem.createFile(
          wsPath,
          new File([''], filePath.fileName, { type: 'text/plain' }),
        );
        toast.success(t.app.toasts.fileCreated({ fileName: filePath.fileName }));
      } catch (error) {
        toast.error(t.app.toasts.fileCreateFailed);
        throw error;
      }

      if (navigate && navigation.isNavigationVersionCurrent(navigationVersion)) {
        navigation.goWsFile(wsPath);
      }
    },
  ),

  c('command::ws:quick-new-file', ({ workspaceState }, { pathPrefix }, key) => {
    const { store, dispatch } = getCtx(key);
    const wsPaths = store.get(workspaceState.$wsPaths) || [];
    const normalizedPrefix = pathPrefix?.replace(/^\/+|\/+$/g, '');
    const untitledFiles = wsPaths
      .map((path) => path.path)
      .filter((path) => {
        const parent = path.includes('/') ? path.slice(0, path.lastIndexOf('/')) : '';
        return parent === (normalizedPrefix ?? '');
      })
      .map((path) => path.slice(path.lastIndexOf('/') + 1))
      .map((name) => name.match(/^untitled-(\d+)\.tex$/u)?.[1])
      .filter((value): value is string => value !== undefined)
      .map((value) => Number.parseInt(value, 10));

    const nextNum = untitledFiles.length > 0 ? Math.max(...untitledFiles) + 1 : 1;
    const fileName = `untitled-${nextNum}.tex`;
    dispatch('command::ws:new-file-from-input', {
      inputPath: normalizedPrefix
        ? WsPath.pathJoin(normalizedPrefix, fileName)
        : fileName,
    });
  }),

  c('command::ws:new-note-from-input', ({ navigation }, { inputPath }, key) => {
    const { dispatch } = getCtx(key);
    validateInputPath(inputPath);

    const { wsName } = navigation.resolveAtoms();

    if (!wsName) {
      throwAppError(
        'error::workspace:not-opened',
        t.app.errors.workspace.notOpened,
        {
          wsPath: inputPath,
        },
      );
    }

    // Add .md extension if not present
    if (!inputPath.endsWith(WsPath.DEFAULT_NOTE_EXTENSION)) {
      inputPath = inputPath + WsPath.DEFAULT_NOTE_EXTENSION;
    }
    const wsPath = WsPath.fromParts(wsName, inputPath).toString();

    dispatch('command::ws:create-note', { wsPath, navigate: true });
  }),

  c(
    'command::ws:create-note',
    async ({ fileSystem, navigation, projectCollaboration }, { wsPath, navigate }) => {
      const parsedPath = WsPath.fromString(wsPath);
      const wsName = parsedPath.wsName;

      if (!wsName) {
        throwAppError(
          'error::workspace:not-opened',
          t.app.errors.workspace.notOpened,
          {
            wsPath,
          },
        );
      }

      const filePath = WsPath.assertFile(wsPath);
      projectCollaboration.assertProjectStructureMutable(filePath.wsName, 'create files');
      const fileNameWithoutExt = filePath.fileNameWithoutExtension;
      const navigationVersion = navigation.captureNavigationVersion();

      try {
        await fileSystem.createFile(
          wsPath,
          new File([''], fileNameWithoutExt, {
            type: 'text/plain',
          }),
        );
        toast.success(
          t.app.toasts.fileCreated({ fileName: filePath.fileName }),
        );
      } catch (error) {
        toast.error(t.app.toasts.fileCreateFailed);
        throw error;
      }

      // A storage write can outlive the dialog. Do not let its completion
      // overwrite a navigation made while the write was pending.
      if (
        navigate &&
        navigation.isNavigationVersionCurrent(navigationVersion)
      ) {
        navigation.goWsPath(wsPath);
      }
    },
  ),

  c('command::ws:go-workspace', ({ navigation }, { wsName }) => {
    navigation.goWorkspace(wsName);
  }),
  c('command::ws:go-ws-path', ({ navigation }, { wsPath }) => {
    navigation.goWsFile(wsPath);
  }),

  c(
    'command::ws:delete-workspace',
    async ({ workspaceOps, navigation }, { wsName }) => {
      await workspaceOps.deleteWorkspaceInfo(wsName);

      if (navigation.resolveAtoms().wsName === wsName) {
        navigation.goHome();
      }
    },
  ),

  c('command::ws:delete-ws-path', async ({ fileSystem, projectCollaboration }, { wsPath }) => {
    const filePath = WsPath.assertFile(wsPath);
    projectCollaboration.assertProjectStructureMutable(filePath.wsName, 'delete files');

    try {
      await fileSystem.deleteFile(wsPath);
      toast.success(t.app.toasts.fileDeleted({ fileName: filePath.fileName }));
    } catch (error) {
      toast.error(t.app.toasts.fileDeleteFailed);
      throw error;
    }
  }),

  c('command::ws:delete-ws-paths', async ({ fileSystem, projectCollaboration }, { wsPaths }) => {
    const uniqueWsPaths = [...new Set(wsPaths)];
    for (const wsPath of uniqueWsPaths) projectCollaboration.assertProjectStructureMutable(WsPath.assertFile(wsPath).wsName, 'delete files');

    if (uniqueWsPaths.length === 0) {
      throwAppError(
        'error::file:invalid-note-path',
        t.app.errors.file.invalidNotePath,
        {
          invalidWsPath: '',
        },
      );
    }

    try {
      await fileSystem.deleteFiles(uniqueWsPaths);
      toast.success(t.app.toasts.filesDeleted({ count: uniqueWsPaths.length }));
    } catch (error) {
      toast.error(t.app.toasts.filesDeleteFailed);
      throw error;
    }
  }),

  c(
    'command::ws:rename-ws-path',
    async (
      { editorEngine, fileSystem, projectCollaboration, userActivityService },
      { wsPath, newWsPath },
    ) => {
      const oldPath = WsPath.fromString(wsPath);
      const newPath = WsPath.fromString(newWsPath);
      projectCollaboration.assertProjectStructureMutable(oldPath.wsName, 'rename files');

      if (!oldPath.wsName) {
        throwAppError(
          'error::workspace:not-opened',
          t.app.errors.workspace.notOpened,
          {
            wsPath,
          },
        );
      }

      if (oldPath.wsName !== newPath.wsName) {
        throwAppError(
          'error::file:invalid-operation',
          t.app.errors.file.cannotRenameToDifferentWorkspace,
          {
            operation: 'rename',
            oldWsPath: wsPath,
            newWsPath,
          },
        );
      }

      const newFilePath = WsPath.assertFile(newWsPath);
      if (wsPath === newWsPath) {
        return;
      }

      await assertSourceSavesDrained(
        editorEngine,
        [{ oldWsPath: wsPath, newWsPath }],
        'rename',
      );

      // Keep the open note visible while the storage write is in flight.
      // WorkspaceState follows the durable rename event once it lands.
      try {
        await fileSystem.renameFile({
          oldWsPath: wsPath,
          newWsPath,
        });
      } catch (error) {
        rethrowRelocationError(
          error,
          newWsPath,
          newFilePath.fileName,
          t.app.toasts.fileRenameFailed,
        );
      }

      const starResult = await userActivityService.relocateStarredItem(
        oldPath,
        newPath,
      );
      if (starResult === 'failed') {
        toast.warning(
          t.app.toasts.fileRenameStarUpdateFailed({
            fileName: newFilePath.fileName,
          }),
        );
      } else {
        toast.success(
          t.app.toasts.fileRenamed({ fileName: newFilePath.fileName }),
        );
      }
    },
  ),

  c(
    'command::ws:move-ws-path',
    async (
      { editorEngine, fileSystem, projectCollaboration, userActivityService },
      { wsPath, destDirWsPath },
    ) => {
      const filePath = WsPath.assertFile(wsPath);
      projectCollaboration.assertProjectStructureMutable(filePath.wsName, 'move files');
      const destDir = WsPath.fromString(
        normalizeDestinationDirWsPath(destDirWsPath),
      ).asDir();
      if (!destDir) {
        throwAppError(
          'error::workspace:not-opened',
          t.app.errors.workspace.notOpened,
          {
            wsPath,
          },
        );
      }

      const newWsPath = WsPath.fromParts(
        destDir.wsName,
        WsPath.pathJoin(destDir.isRoot ? '' : destDir.path, filePath.fileName),
      ).wsPath;

      if (wsPath === newWsPath) {
        return;
      }

      await assertSourceSavesDrained(
        editorEngine,
        [{ oldWsPath: wsPath, newWsPath }],
        'move',
      );

      // Keep the open note visible while the storage write is in flight.
      // WorkspaceState follows the durable rename event once it lands.
      try {
        await fileSystem.renameFile({
          oldWsPath: wsPath,
          newWsPath,
        });
      } catch (error) {
        rethrowRelocationError(
          error,
          newWsPath,
          filePath.fileName,
          t.app.toasts.fileMoveFailed,
        );
      }

      const starResult = await userActivityService.relocateStarredItem(
        filePath,
        WsPath.fromString(newWsPath),
      );
      if (starResult === 'failed') {
        toast.warning(
          t.app.toasts.fileMoveStarUpdateFailed({
            fileName: filePath.fileName,
          }),
        );
      } else {
        toast.success(t.app.toasts.fileMoved({ fileName: filePath.fileName }));
      }
    },
  ),

  c('command::ws:quick-new-note', ({ workspaceState }, { pathPrefix }, key) => {
    const { store, dispatch } = getCtx(key);
    const wsPaths = store.get(workspaceState.$noteWsPaths) || [];

    const untitledNotes = wsPaths
      .map((path) => path.fileNameWithoutExtension)
      .filter((name) => name.startsWith('untitled-'))
      .map((name) => {
        const num = Number.parseInt(name.replace('untitled-', ''), 10);
        return Number.isNaN(num) ? 0 : num;
      });

    const nextNum =
      untitledNotes.length > 0 ? Math.max(...untitledNotes) + 1 : 1;
    const newNoteName = `untitled-${nextNum}`;

    dispatch('command::ws:new-note-from-input', {
      inputPath: pathPrefix
        ? WsPath.pathJoin(pathPrefix, newNoteName)
        : newNoteName,
    });
  }),

  c(
    'command::ws:create-directory',
    async ({ fileSystem, projectCollaboration, projectStorage }, { dirWsPath }) => {
      const dirPath = WsPath.fromString(dirWsPath).asDir();

      if (!dirPath || dirPath.isRoot) {
        throwAppError(
          'error::ws-path:invalid-ws-path',
          t.app.errors.wsPath.invalidDirectoryPath,
          { invalidPath: dirWsPath },
        );
      }

      projectCollaboration.assertProjectStructureMutable(dirPath.wsName, 'create folders');
      const provider = await projectStorage.getProvider(dirPath.wsName);
      await provider.createDirectory(projectDirectoryPath(dirPath.path));
      // FileSystem owns the workspace listing revision; project storage may
      // create a real empty directory without creating a visible file.
      fileSystem.refreshFileTree();
      toast.success(t.app.toasts.folderCreated({ folderName: basename(dirPath.path) }));
    },
  ),

  c(
    'command::ws:rename-directory',
    async (
      {
        editorEngine,
        fileSystem,
        projectStorage,
        projectCollaboration,
        userActivityService,
        workspaceState,
      },
      { oldDirWsPath, newDirWsPath },
      key,
    ) => {
      const { store } = getCtx(key);
      const oldDir = assertDirectoryWsPath(oldDirWsPath);
      const newDir = assertDirectoryWsPath(newDirWsPath);
      projectCollaboration.assertProjectStructureMutable(oldDir.wsName, 'rename folders');

      if (oldDir.wsName !== newDir.wsName) {
        throwAppError(
          'error::file:invalid-operation',
          t.app.errors.file.cannotRenameToDifferentWorkspace,
          {
            oldWsPath: oldDirWsPath,
            newWsPath: newDirWsPath,
            operation: 'rename-directory',
          },
        );
      }

      if (oldDir.path === newDir.path) {
        return;
      }

      if (newDir.path.startsWith(oldDir.path)) {
        throwAppError(
          'error::file:invalid-operation',
          t.app.errors.file.cannotMoveDuringRename,
          {
            oldWsPath: oldDirWsPath,
            newWsPath: newDirWsPath,
            operation: 'rename-directory',
          },
        );
      }

      const wsPaths = store.get(workspaceState.$wsPaths);
      const descendants = wsPaths.filter((path) =>
        isUnderDirectory(WsPath.fromString(path.wsPath), oldDir),
      );

      const provider = await projectStorage.getProvider(oldDir.wsName);

      if (descendants.length === 0) {
        await provider.createDirectory(projectDirectoryPath(newDir.path));
        await provider.deleteDirectory(projectDirectoryPath(oldDir.path));
        fileSystem.refreshFileTree();
        toast.success(
          t.app.toasts.folderRenamed({ folderName: basename(newDir.path) }),
        );
        return;
      }

      const pairs = descendants.map((path) => {
        const filePath = WsPath.assertFile(path.wsPath);
        const suffix = filePath.path.slice(oldDir.path.length);
        return {
          oldWsPath: filePath.wsPath,
          newWsPath: WsPath.fromParts(
            oldDir.wsName,
            WsPath.pathJoin(newDir.path, suffix),
          ).wsPath,
        };
      });

      const oldPathSet = new Set(pairs.map((pair) => pair.oldWsPath));
      const existingPathSet = new Set(wsPaths.map((path) => path.wsPath));
      for (const pair of pairs) {
        if (
          existingPathSet.has(pair.newWsPath) &&
          !oldPathSet.has(pair.newWsPath)
        ) {
          throwAppError(
            'error::file:already-existing',
            t.app.errors.file.alreadyExistsInDest({
              fileName: WsPath.assertFile(pair.newWsPath).fileName,
            }),
            {
              wsPath: pair.newWsPath,
            },
          );
        }
      }

      await assertSourceSavesDrained(editorEngine, pairs, 'rename');

      try {
        await fileSystem.renameFiles(pairs);
        await provider.createDirectory(projectDirectoryPath(newDir.path));
        await provider.deleteDirectory(projectDirectoryPath(oldDir.path));
        fileSystem.refreshFileTree();
      } catch (error) {
        toast.error(t.app.toasts.folderRenameFailed);
        throw error;
      }

      const starResult = await userActivityService.relocateStarredItems(
        pairs.map(({ oldWsPath, newWsPath }) => ({
          oldItem: WsPath.fromString(oldWsPath),
          newItem: WsPath.fromString(newWsPath),
        })),
      );
      const folderName = basename(newDir.path);
      if (starResult === 'failed') {
        toast.warning(
          t.app.toasts.folderRenameStarUpdateFailed({ folderName }),
        );
      } else {
        toast.success(t.app.toasts.folderRenamed({ folderName }));
      }
    },
  ),

  c(
    'command::ws:delete-directory',
    async (
      { fileSystem, projectStorage, projectCollaboration, workspaceState },
      { dirWsPath },
      key,
    ) => {
      const { store } = getCtx(key);
      const dirPath = assertDirectoryWsPath(dirWsPath);
      projectCollaboration.assertProjectStructureMutable(dirPath.wsName, 'delete folders');
      const descendants = store
        .get(workspaceState.$wsPaths)
        .filter((path) =>
          isUnderDirectory(WsPath.fromString(path.wsPath), dirPath),
        );

      const provider = await projectStorage.getProvider(dirPath.wsName);

      // Delete first, then leave the route stable. The editor and asset pages
      // derive existence from workspace-state atoms and render not-found states
      // when the routed file disappears.
      try {
        if (descendants.length > 0) {
          await fileSystem.deleteFiles(descendants.map((path) => path.wsPath));
        }
        await provider.deleteDirectory(projectDirectoryPath(dirPath.path));
        fileSystem.refreshFileTree();
        toast.success(
          t.app.toasts.folderDeleted({ folderName: basename(dirPath.path) }),
        );
      } catch (error) {
        toast.error(t.app.toasts.folderDeleteFailed);
        throw error;
      }
    },
  ),
  c('command::ws:go-ws-home', ({ navigation }) => {
    navigation.goWorkspace();
  }),

  c('command::ws:refresh-file-tree', ({ fileSystem }) => {
    fileSystem.refreshFileTree();
  }),

  c(
    'command::ws:recover-note-snapshot',
    async ({ noteSnapshot, fileSystem, navigation }, { snapshotId }) => {
      const snapshot = await noteSnapshot.getSnapshot(snapshotId);
      if (!snapshot) {
        toast.error(t.app.toasts.snapshotNotFound);
        return;
      }

      const originalPath = WsPath.fromString(snapshot.wsPath).asFile();
      if (!originalPath) {
        toast.error(t.app.toasts.snapshotRecoverFailed);
        return;
      }

      // Recovery always lands in a fresh file beside the original — current
      // content is never overwritten. Preserve the original extension so TeX
      // snapshots recover as .tex/.bib/.sty/etc. rather than becoming Markdown.
      // Strip a previous '-recovered-<n>' suffix so repeated recovery does not
      // stack suffixes.
      const base =
        originalPath.fileNameWithoutExtension.replace(/-recovered-\d+$/, '') ||
        originalPath.fileNameWithoutExtension;

      try {
        let newWsPath: string | undefined;
        for (let attempt = 1; attempt <= 100; attempt++) {
          const candidate = originalPath.replaceFileName(
            `${base}-recovered-${attempt}${originalPath.extension ?? WsPath.DEFAULT_NOTE_EXTENSION}`,
          ).wsPath;
          if (!(await fileSystem.exists(candidate))) {
            newWsPath = candidate;
            break;
          }
        }
        if (!newWsPath) {
          toast.error(t.app.toasts.snapshotRecoverFailed);
          return;
        }

        await fileSystem.createTextFile(newWsPath, snapshot.content);

        toast.success(
          t.app.toasts.snapshotRecovered({
            fileName: WsPath.assertFile(newWsPath).fileName,
          }),
        );
        navigation.goWsPath(newWsPath);
      } catch (error) {
        toast.error(t.app.toasts.snapshotRecoverFailed);
        throw error;
      }
    },
  ),

  c(
    'command::ws:clone-note',
    async ({ workspaceState, fileSystem, navigation }, _args, key) => {
      const { store } = getCtx(key);
      const currentWsPath = store.get(workspaceState.$currentWsFilePath);

      if (!currentWsPath) {
        throwAppError(
          'error::workspace:not-opened',
          t.app.errors.workspace.noNoteOpenToClone,
          {},
        );
      }

      const originalFile = await fileSystem.readFile(currentWsPath.wsPath);
      if (!originalFile) {
        throwAppError(
          'error::file:invalid-note-path',
          t.app.errors.file.originalNoteNotFound,
          { invalidWsPath: currentWsPath.wsPath },
        );
      }

      const base = currentWsPath.fileNameWithoutExtension.replace(
        /-copy-\d+$/u,
        '',
      );
      const extension = currentWsPath.extension ?? '';
      const parentPath = currentWsPath.getParent()?.path;
      const siblingFileNames = new Set(
        store
          .get(workspaceState.$wsPaths)
          .filter((entry) => entry.getParent()?.path === parentPath)
          .map((entry) => entry.fileName),
      );

      let copyNumber = 1;
      let newFileName = `${base}-copy-${copyNumber}${extension}`;
      while (siblingFileNames.has(newFileName)) {
        copyNumber += 1;
        newFileName = `${base}-copy-${copyNumber}${extension}`;
      }

      const newWsPath = currentWsPath.replaceFileName(newFileName).wsPath;
      const bytes = await originalFile.arrayBuffer();
      await fileSystem.createFile(
        newWsPath,
        new File([bytes], newFileName, { type: originalFile.type }),
      );
      navigation.goWsPath(newWsPath);
    },
  ),


  c(
    'command::ws:daily-note',
    async ({ workspaceState, fileSystem, navigation }, args, key) => {
      const { store, dispatch } = getCtx(key);
      const wsName = store.get(workspaceState.$currentWsName);
      const currentWsPath = store.get(workspaceState.$currentWsPath);

      if (!wsName) {
        throwAppError(
          'error::workspace:not-opened',
          t.app.errors.workspace.noWorkspaceForDailyNote,
          {},
        );
      }

      const today = args?.date ? new Date(args.date) : new Date();

      // Format date as YYYY-MMM-DD
      const year = today.getFullYear();
      const monthNames = [
        'Jan',
        'Feb',
        'Mar',
        'Apr',
        'May',
        'Jun',
        'Jul',
        'Aug',
        'Sep',
        'Oct',
        'Nov',
        'Dec',
      ];
      const month = monthNames[today.getMonth()];
      const day = String(today.getDate()).padStart(2, '0');
      const formattedDate = `${year}-${month}-${day}`;
      const fileName = `${formattedDate}-daily${WsPath.DEFAULT_NOTE_EXTENSION}`;

      const parentDir =
        currentWsPath?.getParent() || WsDirPath.fromString(`${wsName}:`);

      const dailyNotePath = parentDir.createFilePath(fileName);
      const dailyNoteWsPath = dailyNotePath.wsPath;

      const exists = await fileSystem.exists(dailyNoteWsPath);

      if (exists) {
        navigation.goWsPath(dailyNoteWsPath);
      } else {
        dispatch('command::ws:create-note', {
          wsPath: dailyNoteWsPath,
          navigate: true,
        });
      }
    },
  ),

  c(
    'command::workspace:toggle-star',
    async (
      { workspaceState, userActivityService },
      { wsPath: argWsPath },
      key,
    ) => {
      const { store } = getCtx(key);
      const currentOpenWsPath = store.get(
        workspaceState.$currentWsFilePath,
      )?.wsPath;
      const wsPathToToggleString = argWsPath ?? currentOpenWsPath;

      if (!wsPathToToggleString) {
        throwAppError(
          'error::workspace:no-note-opened',
          t.app.errors.workspace.noNoteOpened,
          {},
        );
        return;
      }

      await userActivityService.toggleStarItem(
        WsPath.fromString(wsPathToToggleString),
      );
    },
  ),
];
