import {
  AfterViewInit,
  ChangeDetectionStrategy,
  Component,
  computed,
  ElementRef,
  inject,
  input,
  Input,
  OnDestroy,
  output,
  viewChild,
  ViewEncapsulation,
} from '@angular/core';
import { AsyncPipe } from '@angular/common';
import { LocaleDatePipe } from 'src/app/ui/pipes/locale-date.pipe';
import { MatIcon } from '@angular/material/icon';
import {
  MatMenu,
  MatMenuContent,
  MatMenuItem,
  MatMenuTrigger,
} from '@angular/material/menu';
import { MatDivider } from '@angular/material/divider';
import { ESTIMATE_OPTIONS } from '../../add-task-bar/add-task-bar.const';
import { Task, TaskCopy, TaskWithSubTasks } from '../../task.model';
import { EMPTY, forkJoin, from, Observable, of, ReplaySubject, Subject } from 'rxjs';
import {
  concatMap,
  delay,
  distinctUntilChanged,
  first,
  map,
  switchMap,
  take,
  takeUntil,
  tap,
} from 'rxjs/operators';
import { Project } from '../../../project/project.model';
import { TaskService } from '../../task.service';
import { TaskRepeatCfgService } from '../../../task-repeat-cfg/task-repeat-cfg.service';
import { MatDialog } from '@angular/material/dialog';
import { IssueService } from '../../../issue/issue.service';
import { SnackService } from '../../../../core/snack/snack.service';
import { ProjectService } from '../../../project/project.service';
import { _MISSING_PROJECT_, DEFAULT_PROJECT_ICON } from '../../../project/project.const';
import { WorkContextService } from '../../../work-context/work-context.service';
import { GlobalConfigService } from '../../../config/global-config.service';
import { KeyboardConfig } from '@sp/keyboard-config';
import { DialogScheduleTaskComponent } from '../../../planner/dialog-schedule-task/dialog-schedule-task.component';
import { DialogDeadlineComponent } from '../../dialog-deadline/dialog-deadline.component';
import { DialogTimeEstimateComponent } from '../../dialog-time-estimate/dialog-time-estimate.component';
import { throttle } from '../../../../util/decorators';
import { DialogConfirmComponent } from '../../../../ui/dialog-confirm/dialog-confirm.component';
import { Update } from '@ngrx/entity';
import { isTouchActive } from 'src/app/util/input-intent';
import { T } from 'src/app/t.const';
import { TranslateModule, TranslateService } from '@ngx-translate/core';
import { Store } from '@ngrx/store';
import { selectTaskByIdWithSubTaskData } from '../../store/task.selectors';
import { MatIconButton } from '@angular/material/button';
import { MatTooltip } from '@angular/material/tooltip';
import { getDbDateStr } from '../../../../util/get-db-date-str';
import { PlannerActions } from '../../../planner/store/planner.actions';
import { addSubTask } from '../../../tasks/store/task.actions';
import { combineDateAndTime } from '../../../../util/combine-date-and-time';
import { getNextWeekDayOffset } from '../../../../util/get-next-week-day-offset';
import { DateAdapter } from '@angular/material/core';
import { ICAL_TYPE } from '../../../issue/issue.const';
import { IssueIconPipe } from '../../../issue/issue-icon/issue-icon.pipe';
import { showFocusOverlay } from '../../../focus-mode/store/focus-mode.actions';
import { toSignal } from '@angular/core/rxjs-interop';
import { TagService } from '../../../tag/tag.service';
import { DialogPromptComponent } from '../../../../ui/dialog-prompt/dialog-prompt.component';
import { TaskSharedActions } from '../../../../root-store/meta/task-shared.actions';
import { selectTodayTaskIds } from '../../../work-context/store/work-context.selectors';
import { DateService } from '../../../../core/date/date.service';
import { MenuTouchFixDirective } from '../menu-touch-fix.directive';
import { TaskLog } from '../../../../core/log';
import { isTouchEventInstance } from '../../../../util/is-touch-event.util';
import { TaskFocusService } from '../../task-focus.service';
import { DEFAULT_GLOBAL_CONFIG } from 'src/app/features/config/default-global-config.const';
import { MenuTreeService } from '../../../menu-tree/menu-tree.service';
import { SelectOptionRowComponent } from '../../../../ui/select-option-row/select-option-row.component';
import { AddSubtaskInputService } from '../../add-subtask-input/add-subtask-input.service';

@Component({
  selector: 'task-context-menu-inner',
  imports: [
    AsyncPipe,
    MatIcon,
    MatMenu,
    MatMenuContent,
    MatMenuItem,
    MatDivider,
    TranslateModule,
    MatMenuTrigger,
    MatIconButton,
    MatTooltip,
    IssueIconPipe,
    MenuTouchFixDirective,
    SelectOptionRowComponent,
  ],
  templateUrl: './task-context-menu-inner.component.html',
  styleUrl: './task-context-menu-inner.component.scss',
  changeDetection: ChangeDetectionStrategy.OnPush,
  encapsulation: ViewEncapsulation.Emulated,
})
export class TaskContextMenuInnerComponent implements AfterViewInit, OnDestroy {
  private readonly _datePipe = inject(LocaleDatePipe);
  private readonly _taskService = inject(TaskService);
  private readonly _taskRepeatCfgService = inject(TaskRepeatCfgService);
  private readonly _matDialog = inject(MatDialog);
  private readonly _issueService = inject(IssueService);
  private readonly _elementRef = inject(ElementRef);
  private readonly _snackService = inject(SnackService);
  private readonly _projectService = inject(ProjectService);
  private readonly _globalConfigService = inject(GlobalConfigService);
  private readonly _store = inject(Store);
  private readonly _dateAdapter = inject(DateAdapter);
  private readonly _tagService = inject(TagService);
  private readonly _translateService = inject(TranslateService);
  private readonly _workContextService = inject(WorkContextService);
  private readonly _taskFocusService = inject(TaskFocusService);
  private readonly _dateService = inject(DateService);
  private readonly _menuTreeService = inject(MenuTreeService);
  private readonly _addSubtaskInputService = inject(AddSubtaskInputService);

  protected readonly isTouchActive = isTouchActive;
  protected readonly T = T;
  readonly ESTIMATE_OPTIONS = ESTIMATE_OPTIONS;
  readonly DEFAULT_PROJECT_ICON = DEFAULT_PROJECT_ICON;

  isAdvancedControls = input<boolean>(false);
  todayList = toSignal(this._store.select(selectTodayTaskIds), { initialValue: [] });
  isOnTodayList = computed(() => this.task && this.todayList().includes(this.task.id));
  readonly isTimeTrackingEnabled = computed(
    () => this._globalConfigService.appFeatures().isTimeTrackingEnabled,
  );
  readonly isFocusModeEnabled = computed(
    () => this._globalConfigService.appFeatures().isFocusModeEnabled,
  );

  // eslint-disable-next-line @angular-eslint/no-output-native
  close = output();

  contextMenuPosition: { x: string; y: string } = { x: '100px', y: '100px' };

  readonly contextMenuTrigger = viewChild('contextMenuTriggerEl', {
    read: MatMenuTrigger,
  });

  readonly contextMenu = viewChild('contextMenu', { read: MatMenu });

  task!: TaskWithSubTasks | Task;

  isCurrent: boolean = false;
  isBacklog: boolean = false;

  private _task$: ReplaySubject<TaskWithSubTasks | Task> = new ReplaySubject(1);
  issueUrl$: Observable<string | null> = this._task$.pipe(
    switchMap((v) => {
      return v.issueType && v.issueId && v.issueProviderId
        ? from(this._issueService.issueLink(v.issueType, v.issueId, v.issueProviderId))
        : of(null);
    }),
    take(1),
  );
  moveToProjectList$: Observable<Project[]> = this._task$.pipe(
    map((t) => t.projectId),
    distinctUntilChanged(),
    switchMap((pid) =>
      this._projectService.getProjectsWithoutIdInTreeOrder$(pid || null),
    ),
  );
  toggleTagList = this._tagService.tagsNoMyDayAndNoListInTreeOrder;
  projectFolderMap = computed(() => this._menuTreeService.projectFolderMap());
  tagFolderMap = computed(() => this._menuTreeService.tagFolderMap());

  isShowMoveFromAndToBacklogBtns$: Observable<boolean> =
    this._workContextService.activeWorkContext$.pipe(
      take(1),
      map((ctx) => !!ctx.isEnableBacklog),
    );

  private _destroy$: Subject<boolean> = new Subject<boolean>();
  private _isTaskDeleteTriggered: boolean = false;
  private _isOpenedFromKeyboard = false;
  private _touchMenuTimeout: ReturnType<typeof setTimeout> | undefined;
  private _touchMenuRafId: number | undefined;

  // TODO: Skipped for migration because:
  //  Accessor inputs cannot be migrated as they are too complex.
  @Input('task') set taskSet(v: TaskWithSubTasks | Task) {
    this.task = v;
    this.isCurrent = this._taskService.currentTaskId() === v.id;
    this._task$.next(v);
  }

  ngAfterViewInit(): void {
    this.isBacklog = !!this._elementRef.nativeElement.closest('.backlog');

    setTimeout(() => {
      if (!this._isOpenedFromKeyboard) {
        if (document.activeElement instanceof HTMLElement) document.activeElement.blur();
      }
    });
  }

  ngOnDestroy(): void {
    this._destroy$.next(true);
    this._destroy$.complete();
    if (this._touchMenuTimeout !== undefined) {
      clearTimeout(this._touchMenuTimeout);
    }
    if (this._touchMenuRafId !== undefined) {
      cancelAnimationFrame(this._touchMenuRafId);
    }
  }

  open(ev?: MouseEvent | KeyboardEvent | TouchEvent, isOpenedFromKeyBoard = false): void {
    if (ev) {
      ev.preventDefault();
      ev.stopPropagation();
      ev.stopImmediatePropagation();

      if (!isTouchActive() && (ev instanceof MouseEvent || isTouchEventInstance(ev))) {
        const clientX = 'touches' in ev ? ev.touches[0].clientX : ev.clientX;
        const clientY = 'touches' in ev ? ev.touches[0].clientY : ev.clientY;
        this.contextMenuPosition.x = clientX + 10 + 'px';
        const rawY = clientY - 48;
        const safeAreaTop =
          parseInt(
            getComputedStyle(document.documentElement).getPropertyValue(
              '--safe-area-inset-top',
            ),
            10,
          ) || 0;
        this.contextMenuPosition.y = Math.max(rawY, safeAreaTop) + 'px';
      }
    }

    this._isOpenedFromKeyboard = isOpenedFromKeyBoard;
    this.contextMenuTrigger()?.openMenu();
    this._taskFocusService.isTaskContextMenuOpen.set(true);

    if (isTouchActive()) {
      this._touchMenuTimeout = setTimeout(() => {
        const boxes = document.querySelectorAll(
          '.cdk-overlay-connected-position-bounding-box',
        );
        const boundingBox = boxes[boxes.length - 1] as HTMLElement | undefined;
        if (!boundingBox) {
          return;
        }
        // Layout only — CDK backdrop handles scrim + click-to-close
        // pointer-events: none so taps pass through to the backdrop sibling
        boundingBox.style.position = 'fixed';
        boundingBox.style.inset = '0';
        boundingBox.style.width = '';
        boundingBox.style.height = '';
        boundingBox.style.display = 'flex';
        boundingBox.style.justifyContent = 'center';
        boundingBox.style.alignItems = 'flex-end';
        boundingBox.style.pointerEvents = 'none';

        const pane = boundingBox.querySelector('.cdk-overlay-pane') as HTMLElement;
        if (pane) {
          pane.style.position = 'static';
          pane.style.width = '100%';
          pane.style.display = 'flex';
          pane.style.justifyContent = 'center';
          pane.style.pointerEvents = 'none';
        }

        const menuPanel = boundingBox.querySelector('.mat-mdc-menu-panel') as HTMLElement;
        if (menuPanel) {
          // Re-enable pointer events on the menu panel itself
          menuPanel.style.pointerEvents = 'auto';
          menuPanel.style.maxWidth = '300px';
          menuPanel.style.width = '100%';
          menuPanel.style.borderRadius =
            'var(--card-border-radius) var(--card-border-radius) 0 0';
          menuPanel.style.maxHeight = '80vh';
          menuPanel.style.animation = 'none';
          menuPanel.style.transform = 'translateY(24px)';
          menuPanel.style.opacity = '0';
          void menuPanel.offsetHeight;
          menuPanel.style.transition =
            'transform 250ms cubic-bezier(0.2, 0, 0, 1), opacity 150ms ease-out';
          this._touchMenuRafId = requestAnimationFrame(() => {
            menuPanel.style.transform = 'translateY(0)';
            menuPanel.style.opacity = '1';
          });
        }
      });
      this._highlightSourceTask();
    }
  }

  focusRelatedTaskOrNext(): void {
    // Focus the task element after context menu closes
    // Use setTimeout to ensure menu has fully closed and DOM is settled
    setTimeout(() => {
      const taskElement = document.getElementById(`t-${this.task.id}`);
      if (taskElement) {
        // Restore focus to the acted-on task (keyboard continuity) WITHOUT
        // scrolling: an action that relocates the task (e.g. Overdue -> Today)
        // would otherwise yank the viewport to the moved task's new position,
        // breaking "schedule several in a row" triage. The user acted at their
        // current scroll position, so keep them anchored there. See issue #8533.
        taskElement.focus({ preventScroll: true });
        // Ensure focusedTaskId is set even if focus event doesn't fire
        this._taskFocusService.focusedTaskId.set(this.task.id);
      }
    }, 0);
  }

  onClose(): void {
    // Don't manually set focusedTaskId to null here - let the task component's
    // focus/blur handlers manage it automatically to avoid race conditions
    this._taskFocusService.isTaskContextMenuOpen.set(false);
    this.focusRelatedTaskOrNext();
    this.close.emit();
  }

  get kb(): KeyboardConfig {
    if (isTouchActive() || !this.isAdvancedControls()) {
      return {} as any;
    }
    return (this._globalConfigService.cfg()?.keyboard as KeyboardConfig) || {};
  }

  quickAccessKeydown(ev: KeyboardEvent): void {
    const t = ev.target as HTMLElement;
    const btns = Array.from(
      t?.closest('.quick-access')?.querySelectorAll('button') || [],
    );

    const currentIndex = btns.indexOf(t as HTMLButtonElement);

    if (ev.key === 'ArrowRight' && currentIndex < btns.length - 1) {
      (btns[currentIndex + 1] as HTMLElement).focus();
    } else if (ev.key === 'ArrowLeft' && currentIndex > 0) {
      (btns[currentIndex - 1] as HTMLElement).focus();
    }
  }

  focusFirstBtn(ev: FocusEvent): void {
    const t = ev.target as HTMLElement;
    t?.parentElement?.querySelector('button')?.focus();
  }

  focusFirstSubmenuItem(menu: MatMenu): void {
    menu.focusFirstItem('program');
  }

  goToFocusMode(): void {
    this._taskService.setCurrentId(this.task.id);
    this._store.dispatch(showFocusOverlay());
  }

  scheduleTask(): void {
    this._matDialog
      .open(DialogScheduleTaskComponent, {
        // we focus inside dialog instead
        autoFocus: false,
        data: { task: this.task },
      })
      .afterClosed()
      // .pipe(takeUntil(this._destroy$))
      .subscribe((isPlanned) => {
        this.focusRelatedTaskOrNext();
      });
  }

  openDeadlineDialog(): void {
    this._matDialog
      .open(DialogDeadlineComponent, {
        autoFocus: false,
        data: { task: this.task },
      })
      .afterClosed()
      .subscribe(() => {
        this.focusRelatedTaskOrNext();
      });
  }

  removeDeadline(): void {
    this._store.dispatch(
      TaskSharedActions.removeDeadline({
        taskId: this.task.id,
      }),
    );
  }

  updateIssueData(): void {
    this._issueService.refreshIssueTask(this.task, true, true);
  }

  async deleteTask(): Promise<void> {
    // NOTE: prevents attempts to delete the same task multiple times
    if (this._isTaskDeleteTriggered) {
      return;
    }

    const isConfirmBeforeDelete =
      this._globalConfigService.cfg()?.tasks?.isConfirmBeforeDelete ?? true;

    if (isConfirmBeforeDelete) {
      this._matDialog
        .open(DialogConfirmComponent, {
          data: {
            okTxt: T.F.TASK.D_CONFIRM_DELETE.OK,
            message: T.F.TASK.D_CONFIRM_DELETE.MSG,
            translateParams: { title: this.task.title },
          },
        })
        .afterClosed()
        .subscribe(async (isConfirm) => {
          if (isConfirm) {
            await this._performDelete();
          }
        });
    } else {
      await this._performDelete();
    }
  }

  private async _performDelete(): Promise<void> {
    this._isTaskDeleteTriggered = true;
    const taskWithSubTasks = await this._getTaskWithSubtasks();
    this._taskService.remove(taskWithSubTasks);
  }

  startTask(): void {
    this._taskService.setCurrentId(this.task.id);
  }

  pauseTask(): void {
    this._taskService.pauseCurrent();
  }

  estimateTime(): void {
    this._matDialog
      .open(DialogTimeEstimateComponent, {
        data: { task: this.task },
      })
      .afterClosed()
      .subscribe(() => this.focusRelatedTaskOrNext());
  }

  setEstimate(ms: number): void {
    if (ms === this.task.timeEstimate) {
      return;
    }
    this._taskService.update(this.task.id, { timeEstimate: ms });
  }

  addSubTask(): void {
    this._addSubtaskInputService.requestOpen(this.task.parentId || this.task.id);
  }

  async duplicate(): Promise<void> {
    const taskData = {
      isDone: false,
      projectId: this.task.projectId || undefined,
      tagIds: this.task.tagIds || [],
      ...(this.task.notes && { notes: this.task.notes }),
    };
    const timeData = {
      ...(this.task.dueDay && { dueDay: this.task.dueDay }),
      ...(this.task.dueWithTime && { dueWithTime: this.task.dueWithTime }),
      ...(this.task.timeEstimate && { timeEstimate: this.task.timeEstimate }),
    };
    const taskId = this._taskService.add(
      `${this.task.title} (copy)`,
      false,
      { ...taskData, ...timeData },
      false,
    );
    if (this.task.subTaskIds.length) {
      const taskWithSubtasks = await this._getTaskWithSubtasks();
      for (const subTask of taskWithSubtasks.subTasks) {
        const subTaskInfo = {
          isDone: subTask.isDone,
          projectId: subTask.projectId,
          timeEstimate: subTask.timeEstimate,
          notes: subTask.notes,
        };
        const subTaskObj = this._taskService.createNewTaskWithDefaults({
          title: subTask.title,
          additional: subTaskInfo,
        });
        this._store.dispatch(
          addSubTask({
            task: subTaskObj,
            parentId: taskId,
          }),
        );
      }
    }
  }

  moveToTop(): void {
    this._taskService.moveToTop(this.task.id, this.task.parentId, false);
  }

  @throttle(200, { leading: true, trailing: false })
  toggleDoneKeyboard(): void {
    this.toggleTaskDone();
  }

  toggleTaskDone(): void {
    if (this.task.isDone) {
      this._taskService.setUnDone(this.task.id);
    } else {
      this._taskService.setDone(this.task.id);
    }
  }

  addToMyDay(): void {
    this._store.dispatch(
      TaskSharedActions.planTasksForToday({
        taskIds: [this.task.id],
        today: this._dateService.todayStr(),
        startOfNextDayDiffMs: this._dateService.getStartOfNextDayDiffMs(),
        parentTaskMap: { [this.task.id]: this.task.parentId },
        isShowSnack: true,
      }),
    );
  }

  unschedule(): void {
    this._store.dispatch(
      TaskSharedActions.unscheduleTask({
        id: this.task.id,
      }),
    );
  }

  convertToMainTask(): void {
    this._taskService.convertToMainTask(this.task);
  }

  onTagsUpdated(tagIds: string[]): void {
    this._taskService.updateTags(this.task, tagIds);
  }

  toggleTag(tagId: string): void {
    const task = this.task;
    const tagIds = task.tagIds.includes(tagId)
      ? task.tagIds.filter((id) => id !== tagId)
      : [...task.tagIds, tagId];

    this.onTagsUpdated(tagIds);
  }

  openAddNewTag(): void {
    this._matDialog
      .open(DialogPromptComponent, {
        data: {
          placeholder: T.F.TAG.TTL.ADD_NEW_TAG,
        },
      })
      .afterClosed()
      .subscribe((val) => {
        if (val) {
          const t = this.task;
          const newTagId = this._tagService.addTag({
            title: val,
          });
          this._taskService.updateTags(t, [...t.tagIds, newTagId]);
        }
      });
  }

  // TODO move to service
  async moveTaskToProject(projectId: string): Promise<void> {
    if (projectId === this.task.projectId) {
      return;
    } else if (!this.task.repeatCfgId) {
      const taskWithSubTasks = await this._getTaskWithSubtasks();
      this._taskService.moveToProject(taskWithSubTasks, projectId);
      this.onClose();
    } else {
      const taskWithSubTasks = await this._getTaskWithSubtasks();

      forkJoin([
        this._taskRepeatCfgService
          .getTaskRepeatCfgByIdAllowUndefined$(this.task.repeatCfgId)
          .pipe(first()),
        this._taskService
          .getTasksWithSubTasksByRepeatCfgId$(this.task.repeatCfgId)
          .pipe(first()),
        this._taskService.getArchiveTasksForRepeatCfgId(this.task.repeatCfgId),
        this._projectService.getByIdOnce$(projectId),
      ])
        .pipe(
          concatMap(
            ([
              reminderCfg,
              nonArchiveInstancesWithSubTasks,
              archiveInstances,
              targetProject,
            ]) => {
              TaskLog.log({
                reminderCfg,
                nonArchiveInstancesWithSubTasks,
                archiveInstances,
              });

              // Repeat config was deleted (e.g. via cross-client sync) but the task
              // still references it — treat it as a plain task move instead of
              // crashing on the missing config. (#8715)
              if (!reminderCfg) {
                this._taskService.moveToProject(taskWithSubTasks, projectId);
                this.onClose();
                return EMPTY;
              }

              // if there is only a single instance (probably just created) than directly update the task repeat cfg
              if (
                nonArchiveInstancesWithSubTasks.length === 1 &&
                archiveInstances.length === 0
              ) {
                this._taskRepeatCfgService.updateTaskRepeatCfg(reminderCfg.id, {
                  projectId,
                });
                this._taskService.moveToProject(taskWithSubTasks, projectId);
                this.onClose();
                return EMPTY;
              }

              return this._matDialog
                .open(DialogConfirmComponent, {
                  data: {
                    okTxt: T.F.TASK_REPEAT.D_CONFIRM_MOVE_TO_PROJECT.OK,
                    message: T.F.TASK_REPEAT.D_CONFIRM_MOVE_TO_PROJECT.MSG,
                    translateParams: {
                      projectName: targetProject?.title ?? _MISSING_PROJECT_,
                      tasksNr:
                        nonArchiveInstancesWithSubTasks.length + archiveInstances.length,
                    },
                  },
                })
                .afterClosed()
                .pipe(
                  tap((isConfirm) => {
                    if (isConfirm) {
                      this._taskRepeatCfgService.updateTaskRepeatCfg(reminderCfg.id, {
                        projectId,
                      });
                      nonArchiveInstancesWithSubTasks.forEach((nonArchiveTask) => {
                        this._taskService.moveToProject(nonArchiveTask, projectId);
                      });

                      const archiveUpdates: Update<TaskCopy>[] = [];
                      archiveInstances.forEach((archiveTask) => {
                        archiveUpdates.push({
                          id: archiveTask.id,
                          changes: { projectId },
                        });
                        if (archiveTask.subTaskIds.length) {
                          archiveTask.subTaskIds.forEach((subId) => {
                            archiveUpdates.push({
                              id: subId,
                              changes: { projectId },
                            });
                          });
                        }
                      });
                      this._taskService.updateArchiveTasks(archiveUpdates);
                    }
                  }),
                );
            },
          ),
        )
        .subscribe(() => this.onClose());
    }
  }

  moveToBacklog(): void {
    if (this.task.projectId && !this.task.parentId) {
      // Moving to the backlog is a list-position change only; it must not
      // alter the task's schedule (#8592).
      this._projectService.moveTaskToBacklog(this.task.id, this.task.projectId);
    }
  }

  moveToToday(): void {
    if (this.task.projectId && !this.task.parentId) {
      // Moving to the regular list is a list-position change only; it must not
      // schedule the task for today (#8592).
      this._projectService.moveTaskToTodayList(this.task.id, this.task.projectId);
    }
  }

  trackByProjectId(i: number, project: Project): string {
    return project.id;
  }

  private _highlightSourceTask(): void {
    const taskEl = document.getElementById(`t-${this.task.id}`);
    if (!taskEl) {
      return;
    }
    taskEl.classList.add('context-menu-highlight');
    this.contextMenu()
      ?.closed.pipe(takeUntil(this._destroy$), first())
      .subscribe(() => {
        taskEl.classList.remove('context-menu-highlight');
      });
  }

  private async _getTaskWithSubtasks(): Promise<TaskWithSubTasks> {
    return await this._store
      .select(selectTaskByIdWithSubTaskData, { id: this.task.id })
      .pipe(
        first(),
        // NOTE without the delay selectTaskByIdWithSubTaskData triggers twice for unknown reasons
        delay(50),
      )
      .toPromise();
  }

  quickAccessBtnClick(item: number): void {
    const tDate = new Date();
    tDate.setMinutes(0, 0, 0);
    switch (item) {
      case 1:
        this._schedule(tDate);
        break;
      case 2:
        const tomorrow = tDate;
        tomorrow.setDate(tomorrow.getDate() + 1);
        this._schedule(tomorrow);
        break;
      case 3:
        const nextFirstDayOfWeek = tDate;
        const dayOffset = getNextWeekDayOffset(this._dateAdapter, nextFirstDayOfWeek);
        nextFirstDayOfWeek.setDate(nextFirstDayOfWeek.getDate() + dayOffset);
        this._schedule(nextFirstDayOfWeek);
        break;
    }
  }

  private async _schedule(selectedDate: Date, isRemoveFromToday = false): Promise<void> {
    if (!selectedDate) {
      TaskLog.err('no selected date');
      return;
    }

    const newDayDate = new Date(selectedDate);
    const newDay = getDbDateStr(newDayDate);

    // If task is already scheduled for today with time and we're planning for today,
    // just add to my day (which clears the reminder) instead of rescheduling
    if (
      this.task.dueWithTime &&
      this._dateService.isToday(this.task.dueWithTime) &&
      newDay === this._dateService.todayStr()
    ) {
      this.addToMyDay();
      return;
    }

    if (isRemoveFromToday) {
      this.unschedule();
    } else if (this.task.dueDay === newDay) {
      const formattedDate =
        newDay === this._dateService.todayStr()
          ? this._translateService.instant(T.G.TODAY_TAG_TITLE)
          : (this._datePipe.transform(newDay, 'shortDate') as string);
      this._snackService.open({
        type: 'CUSTOM',
        ico: 'info',
        msg: T.F.PLANNER.S.TASK_ALREADY_PLANNED,
        translateParams: { date: formattedDate },
      });
      return;
    } else if (this.task.dueWithTime) {
      const task = this.task;
      const newDate = combineDateAndTime(newDayDate, new Date(this.task.dueWithTime));
      this._taskService.scheduleTask(
        task,
        newDate.getTime(),
        this._globalConfigService.cfg()?.reminder.defaultTaskRemindOption ??
          DEFAULT_GLOBAL_CONFIG.reminder.defaultTaskRemindOption!,
        false,
      );
    } else {
      if (newDay === this._dateService.todayStr()) {
        this.addToMyDay();
      } else {
        this._store.dispatch(
          PlannerActions.planTaskForDay({
            task: this.task,
            day: newDay,
            isShowSnack: true,
          }),
        );
      }
    }
  }

  unscheduleTask(): void {
    this._store.dispatch(
      TaskSharedActions.unscheduleTask({
        id: this.task.id,
      }),
    );
  }

  protected readonly ICAL_TYPE = ICAL_TYPE;
}
