import {
  AfterViewInit,
  ChangeDetectionStrategy,
  Component,
  computed,
  DestroyRef,
  ElementRef,
  HostListener,
  inject,
  input,
  OnDestroy,
  OnInit,
  signal,
  viewChild,
  viewChildren,
} from '@angular/core';
import { HideSubTasksMode, TaskDetailTargetPanel, TaskWithSubTasks } from '../task.model';
import { IssueService } from '../../issue/issue.service';
import { TaskAttachmentService } from '../task-attachment/task-attachment.service';
import { of } from 'rxjs';
import {
  catchError,
  delay,
  distinctUntilChanged,
  map,
  shareReplay,
  skip,
  switchMap,
} from 'rxjs/operators';
import { T } from '../../../t.const';
import { TaskService } from '../task.service';
import {
  expandAnimation,
  expandFadeAnimation,
  expandFadeInOnlyAnimation,
} from '../../../ui/animations/expand.ani';
import { fadeAnimation } from '../../../ui/animations/fade.ani';
import { swirlAnimation } from '../../../ui/animations/swirl-in-out.ani';
import { DialogTimeEstimateComponent } from '../dialog-time-estimate/dialog-time-estimate.component';
import { MatDialog } from '@angular/material/dialog';
import { DialogEditTaskRepeatCfgComponent } from '../../task-repeat-cfg/dialog-edit-task-repeat-cfg/dialog-edit-task-repeat-cfg.component';
import { TaskRepeatCfgService } from '../../task-repeat-cfg/task-repeat-cfg.service';
import { DialogEditTaskAttachmentComponent } from '../task-attachment/dialog-edit-attachment/dialog-edit-task-attachment.component';
import { TaskDetailItemComponent } from './task-additional-info-item/task-detail-item.component';
import { IssueData, IssueProviderJira } from '../../issue/issue.model';
import { ICAL_TYPE, JIRA_TYPE } from '../../issue/issue.const';
import { HISTORY_STATE, IS_ELECTRON } from '../../../app.constants';
import { LayoutService } from '../../../core-ui/layout/layout.service';
import { devError } from '../../../util/dev-error';
import { IS_MOBILE } from '../../../util/is-mobile';
import { GlobalConfigService } from '../../config/global-config.service';
import { DEFAULT_GLOBAL_CONFIG } from '../../config/default-global-config.const';
import { TranslatePipe, TranslateService } from '@ngx-translate/core';
import { getTaskRepeatInfoText } from './get-task-repeat-info-text.util';
import { DateTimeFormatService } from '../../../core/date-time-format/date-time-format.service';
import { IS_TOUCH_PRIMARY } from '../../../util/is-mouse-primary';
import { DialogScheduleTaskComponent } from '../../planner/dialog-schedule-task/dialog-schedule-task.component';
import { DialogDeadlineComponent } from '../dialog-deadline/dialog-deadline.component';
import { MatIconButton } from '@angular/material/button';
import { MatTooltip } from '@angular/material/tooltip';
import { TaskSharedActions } from '../../../root-store/meta/task-shared.actions';
import { Store } from '@ngrx/store';
import { selectIssueProviderById } from '../../issue/store/issue-provider.selectors';
import { IssueLog } from '../../../core/log';
import { TaskTitleComponent } from '../../../ui/task-title/task-title.component';
import { MatIcon } from '@angular/material/icon';
import { TaskListComponent } from '../task-list/task-list.component';
import { MatButton } from '@angular/material/button';
import { ProgressBarComponent } from '../../../ui/progress-bar/progress-bar.component';
import { IssueHeaderComponent } from '../../issue/issue-header/issue-header.component';
import { MatProgressBar } from '@angular/material/progress-bar';
import { IssueContentComponent } from '../../issue/issue-content/issue-content.component';
import { InlineMarkdownComponent } from '../../../ui/inline-markdown/inline-markdown.component';
import { TaskAttachmentListComponent } from '../task-attachment/task-attachment-list/task-attachment-list.component';
import { TagEditComponent } from '../../tag/tag-edit/tag-edit.component';
import { DialogSelectDateTimeComponent } from '../dialog-select-date-time/dialog-select-date-time.component';
import { LocaleDatePipe } from 'src/app/ui/pipes/locale-date.pipe';
import { LocalDateStrPipe } from 'src/app/ui/pipes/local-date-str.pipe';
import { MsToStringPipe } from '../../../ui/duration/ms-to-string.pipe';
import { IssueIconPipe } from '../../issue/issue-icon/issue-icon.pipe';
import { takeUntilDestroyed, toObservable, toSignal } from '@angular/core/rxjs-interop';
import { getDbDateStr, isDBDateStr } from '../../../util/get-db-date-str';
import { isDeadlineOverdue as isDeadlineOverdueFn } from '../util/is-deadline-overdue';
import { isMarkdownChecklist } from '../../markdown-checklist/is-markdown-checklist';
import { Log } from '../../../core/log';
import { isInputElement } from '../../../util/dom-element';
import { clipboardHasText } from '../../../util/clipboard-has-text';
import { checkKeyCombo } from '../../../util/check-key-combo';
import { IS_MAC } from '../../../util/is-mac';
import { ClipboardImageService } from '../../../core/clipboard-image/clipboard-image.service';
import { DropPasteIcons } from '../../../core/drop-paste-input/drop-paste.model';
import {
  AddSubtaskInputComponent,
  AddSubtaskInputCloseReason,
} from '../add-subtask-input/add-subtask-input.component';

@Component({
  selector: 'task-detail-panel',
  templateUrl: './task-detail-panel.component.html',
  styleUrls: ['./task-detail-panel.component.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush,
  animations: [
    expandAnimation,
    expandFadeAnimation,
    expandFadeInOnlyAnimation,
    fadeAnimation,
    swirlAnimation,
  ],
  imports: [
    TaskTitleComponent,
    TaskDetailItemComponent,
    MatIcon,
    MatIconButton,
    MatTooltip,
    TaskListComponent,
    MatButton,
    ProgressBarComponent,
    IssueHeaderComponent,
    MatProgressBar,
    IssueContentComponent,
    InlineMarkdownComponent,
    TaskAttachmentListComponent,
    TagEditComponent,
    LocaleDatePipe,
    LocalDateStrPipe,
    MsToStringPipe,
    TranslatePipe,
    IssueIconPipe,
    AddSubtaskInputComponent,
  ],
})
export class TaskDetailPanelComponent implements OnInit, AfterViewInit, OnDestroy {
  // Services
  attachmentService = inject(TaskAttachmentService);
  taskService = inject(TaskService);
  layoutService = inject(LayoutService);

  private _clipboardImageService = inject(ClipboardImageService);
  private _globalConfigService = inject(GlobalConfigService);
  private _issueService = inject(IssueService);
  private _taskRepeatCfgService = inject(TaskRepeatCfgService);
  private _matDialog = inject(MatDialog);
  private _store = inject(Store);
  private _translateService = inject(TranslateService);
  private _destroyRef = inject(DestroyRef);
  private _dateTimeFormatService = inject(DateTimeFormatService);

  // Exposed so the template can pass the reactive locale to the now-pure
  // `localeDate` pipe, preserving re-render on a locale change.
  readonly locale = this._dateTimeFormatService.currentLocale;

  // Inputs
  task = input.required<TaskWithSubTasks>();
  isOver = input<boolean>(false);
  isDialogMode = input<boolean>(false);

  // View children
  itemEls = viewChildren(TaskDetailItemComponent);
  attachmentPanelElRef = viewChild<TaskDetailItemComponent>('attachmentPanelElRef');
  noteWrapperElRef = viewChild<TaskDetailItemComponent>('noteWrapperElRef');
  addSubtaskInput = viewChild(AddSubtaskInputComponent);
  addSubTaskBtn = viewChild<ElementRef<HTMLButtonElement>>('addSubTaskBtn');

  // The detail panel hosts its own inline subtask draft input rather than
  // delegating to the <task> row that renders the parent: in the Planner (and
  // other non-list views) that row does not exist, so the delegated request was
  // silently dropped (#8617). isSubTasksExpanded controls the sub-task section
  // so the input is visible even when triggered while the section is collapsed.
  readonly isAddSubtaskInputVisible = signal(false);
  readonly isSubTasksExpanded = signal(false);

  // Constants
  IS_TOUCH_PRIMARY = IS_TOUCH_PRIMARY;
  ShowSubTasksMode = HideSubTasksMode;
  T = T;
  ICAL_TYPE = ICAL_TYPE;
  pasteImageHintKey = IS_MAC
    ? T.F.TASK.ADDITIONAL_INFO.PASTE_IMAGE_HINT_MAC
    : T.F.TASK.ADDITIONAL_INFO.PASTE_IMAGE_HINT;

  // Panel state signals grouped together
  panelState = {
    selectedItemIndex: signal(0),
    isFocusNotes: signal(false),
    isDragOver: signal(false),
    isExpandedAttachmentPanel: signal(!IS_MOBILE),
  };

  // Observable conversions
  private _task$ = toObservable(this.task);
  private _taskDetailTargetPanel = toSignal(
    this.taskService.taskDetailPanelTargetPanel$,
    {
      initialValue: null,
    },
  );

  @HostListener('keydown', ['$event'])
  onKeydown(ev: KeyboardEvent): void {
    // Skip handling inside input elements
    const target = ev.target as HTMLElement;
    if (isInputElement(target)) return;

    const cfg = this._globalConfigService.cfg();
    if (!cfg) throw new Error('No config service available');

    const keys = cfg.keyboard;
    if (checkKeyCombo(ev, keys.taskToggleDetailPanelOpen)) {
      this.collapseParent();
    } else if (checkKeyCombo(ev, keys.taskAddSubTask)) {
      // Opening the panel auto-focuses a detail item, so focus is inside the
      // panel rather than on a <task> row. The global task-shortcut handler
      // can't resolve a focused task in that state and drops the shortcut, so
      // handle add-subtask here. stopPropagation prevents the document-level
      // handler from adding a second subtask when focus is on an in-panel row.
      ev.preventDefault();
      ev.stopPropagation();
      this.addSubTask();
    }
  }

  // Parent task data
  parentTaskData = toSignal(
    this._task$.pipe(
      map((task) => task.parentId),
      distinctUntilChanged(),
      switchMap((parentId) =>
        parentId ? this.taskService.getByIdWithSubTaskData$(parentId) : of(null),
      ),
    ),
    { initialValue: null },
  );

  // Repeat config label
  private _repeatCfg$ = this._task$.pipe(
    map((task) => task.repeatCfgId),
    distinctUntilChanged(),
    switchMap((repeatCfgId) =>
      repeatCfgId
        ? this._taskRepeatCfgService.getTaskRepeatCfgByIdAllowUndefined$(repeatCfgId)
        : of(null),
    ),
  );

  repeatCfgLabel = toSignal(
    this._repeatCfg$.pipe(
      map((repeatCfg) => {
        if (!repeatCfg) {
          return null;
        }
        const [key, params] = getTaskRepeatInfoText(
          repeatCfg,
          this._dateTimeFormatService.currentLocale(),
          this._dateTimeFormatService,
          this._translateService,
        );
        return this._translateService.instant(key, params);
      }),
    ),
    { initialValue: null },
  );

  // Issue data reactive loading (replacing async effect)
  private _issueData$ = this._task$.pipe(
    takeUntilDestroyed(this._destroyRef),
    // Only react to changes in issue-related properties
    map((task) => ({
      issueId: task.issueId,
      issueType: task.issueType,
      issueProviderId: task.issueProviderId,
    })),
    distinctUntilChanged((prev, curr) => prev.issueId === curr.issueId),
    switchMap(({ issueId, issueType, issueProviderId }) => {
      if (!issueId || !issueType || !issueProviderId) {
        return of(null);
      }

      if (issueType === ICAL_TYPE) {
        return of(null);
      }

      return this._issueService.getById$(issueType, issueId, issueProviderId).pipe(
        takeUntilDestroyed(this._destroyRef),
        catchError((err) => {
          Log.warn(`Failed to load issue data for ${issueType}#${issueId}`, err);
          return of(null);
        }),
      );
    }),
    shareReplay(1),
  );

  issueData = toSignal(this._issueData$, {
    initialValue: null as IssueData | null,
  });

  isIssueDataLoadedForCurrentType = computed(() => {
    const data = this.issueData();
    return data !== null;
  });

  // Issue attachments
  issueAttachments = computed(() => {
    const data = this.issueData();
    const task = this.task();
    if (data && task.issueType) {
      return this._issueService.getMappedAttachments(task.issueType, data);
    }
    return [];
  });

  // Default task notes computed signal
  defaultTaskNotes = computed(() => {
    const tasks = this._globalConfigService.tasks();
    return tasks?.notesTemplate || '';
  });

  // True only for the app's generic stock template (not a user-customized one).
  // Used to let the checklist button replace the shown default text with a fresh
  // checklist; customized templates are treated as real content and preserved.
  isStockNotesTemplate = computed(
    () => this.defaultTaskNotes() === DEFAULT_GLOBAL_CONFIG.tasks.notesTemplate,
  );

  // Local attachments computed signal
  localAttachments = computed(() => {
    return this.task().attachments || [];
  });

  // Panel expansion computed signals
  isExpandedIssuePanel = computed(() => {
    return !IS_MOBILE && !!this.issueData();
  });

  isExpandedNotesPanel = computed(() => {
    if (this._taskDetailTargetPanel() === TaskDetailTargetPanel.Notes) {
      return true;
    }

    const task = this.task();
    return IS_MOBILE
      ? this.isMarkdownChecklist()
      : !!task.notes || (!task.issueId && !task.attachments?.length);
  });

  // Task-based computed signals
  isMarkdownChecklist = computed(() => {
    const notes = this.task().notes;
    return isMarkdownChecklist(notes || '');
  });

  isPlannedForTodayDay = computed(() => {
    const task = this.task();
    return !!task.dueDay && task.dueDay === getDbDateStr();
  });

  progress = computed(() => {
    const task = this.task();
    return (task && task.timeEstimate && (task.timeSpent / task.timeEstimate) * 100) || 0;
  });

  isOverdue = computed(() => {
    const t = this.task();
    return !!(
      !t.isDone &&
      ((t.dueWithTime && t.dueWithTime < Date.now()) ||
        (t.dueDay &&
          isDBDateStr(t.dueDay) &&
          t.dueDay !== getDbDateStr() &&
          t.dueDay < getDbDateStr()))
    );
  });

  // Template helper computed signals
  isShowSubTasksPanel = computed(() => {
    const task = this.task();
    return task && !task.parentId;
  });

  showTimeEstimate = computed(() => !this.task().subTasks?.length);

  hasAttachments = computed(() => {
    return this.issueAttachments().length > 0 || this.localAttachments().length > 0;
  });

  totalAttachments = computed(() => {
    return this.issueAttachments().length + this.localAttachments().length;
  });

  showScheduleIcon = computed(() => {
    const task = this.task();
    if (task.dueDay) return 'today';
    if (task.dueWithTime && !task.remindAt) return 'schedule';
    return 'alarm';
  });

  scheduleLabelKey = computed(() => {
    const task = this.task();
    return task.dueWithTime || task.dueDay
      ? this.T.F.TASK.ADDITIONAL_INFO.DUE
      : this.T.F.TASK.ADDITIONAL_INFO.SCHEDULE_TASK;
  });

  isDeadlineOverdue = computed(() => isDeadlineOverdueFn(this.task(), getDbDateStr()));

  deadlineLabelKey = computed(() => {
    const t = this.task();
    if (t.deadlineWithTime || t.deadlineDay) {
      return this.isDeadlineOverdue()
        ? this.T.F.TASK.ADDITIONAL_INFO.DEADLINE_OVERDUE
        : this.T.F.TASK.ADDITIONAL_INFO.DEADLINE_DUE_BY;
    }
    return this.T.F.TASK.ADDITIONAL_INFO.DEADLINE;
  });

  // EFFECTS
  // -------
  private _jiraImageHeaders = IS_ELECTRON
    ? this._task$
        .pipe(
          map((task) => ({
            issueType: task.issueType,
            issueProviderId: task.issueProviderId,
          })),
          distinctUntilChanged(
            (prev, curr) =>
              prev.issueType === curr.issueType &&
              prev.issueProviderId === curr.issueProviderId,
          ),
          map(({ issueType, issueProviderId }) =>
            issueType === JIRA_TYPE && issueProviderId ? issueProviderId : null,
          ),
          distinctUntilChanged(),
          switchMap((issueProviderId) =>
            issueProviderId
              ? this._store
                  .select(
                    selectIssueProviderById<IssueProviderJira>(issueProviderId, 'JIRA'),
                  )
                  .pipe(
                    // Orphan issueProviderId — see #7135.
                    catchError((err: unknown) => {
                      IssueLog.warn('Jira header setup skipped', err);
                      return of(null);
                    }),
                  )
              : of(null),
          ),
          takeUntilDestroyed(this._destroyRef),
        )
        .subscribe((jiraCfg) => {
          if (jiraCfg?.isEnabled) {
            window.ea.jiraSetupImgHeaders({ jiraCfg });
          }
        })
    : null;

  private _focusOnTaskIdChange = this._task$
    .pipe(
      map((task) => task.id),
      distinctUntilChanged(),
      skip(1), // Skip initial emission
      takeUntilDestroyed(this._destroyRef),
    )
    .subscribe(() => {
      // Don't carry a half-open subtask draft or the expanded sub-task section
      // over to the next task (the panel component is reused across tasks,
      // unlike per-row <task> components).
      this.isAddSubtaskInputVisible.set(false);
      this.isSubTasksExpanded.set(false);
      // Only auto-focus panel content when focus is already inside the panel,
      // to avoid stealing focus from the main task list during navigation (#6578)
      if (document.activeElement?.closest('task-detail-panel')) {
        this._focusFirst();
      }
    });
  // -------

  private _focusTimeout?: number;
  private _dragEnterTarget?: HTMLElement;

  @HostListener('dragenter', ['$event']) onDragEnter(ev: DragEvent): void {
    this._dragEnterTarget = ev.target as HTMLElement;
    ev.preventDefault();
    ev.stopPropagation();
    this.panelState.isDragOver.set(true);
  }

  @HostListener('dragleave', ['$event']) onDragLeave(ev: DragEvent): void {
    if (this._dragEnterTarget === (ev.target as HTMLElement)) {
      ev.preventDefault();
      ev.stopPropagation();
      this.panelState.isDragOver.set(false);
    }
  }

  @HostListener('drop', ['$event']) onDrop(ev: DragEvent): void {
    this.attachmentService.createFromDrop(ev, this.task().id);
    ev.stopPropagation();
    this.panelState.isDragOver.set(false);
  }

  @HostListener('paste', ['$event']) async onPaste(ev: ClipboardEvent): Promise<void> {
    // Let existing textarea/input/contenteditable paste handlers work normally
    const target = ev.target as HTMLElement;
    if (isInputElement(target)) {
      return;
    }

    // Prioritize text over images (e.g. OneNote puts both on the clipboard).
    // Otherwise a text paste would be silently turned into an image attachment.
    if (clipboardHasText(ev.clipboardData)) {
      return;
    }

    const progress = this._clipboardImageService.handlePasteWithProgress(ev);
    if (!progress) return;

    ev.preventDefault();
    try {
      const result = await progress.resultPromise;
      if (result.success && result.imageUrl) {
        this.attachmentService.addAttachment(this.task().id, {
          id: null,
          type: 'IMG',
          path: result.imageUrl,
          title: this._translateService.instant(
            T.F.TASK.ADDITIONAL_INFO.PASTED_IMAGE_TITLE,
          ),
          icon: DropPasteIcons.IMG,
        });
      }
    } catch (err) {
      Log.err('[CLIPBOARD] Paste attachment failed:', err);
    }
  }

  @HostListener('window:popstate') onBack(): void {
    this.collapseParent();
  }

  ngOnInit(): void {
    window.history.pushState({ [HISTORY_STATE.TASK_DETAIL_PANEL]: true }, '');
  }

  ngAfterViewInit(): void {
    this.taskService.taskDetailPanelTargetPanel$
      .pipe(takeUntilDestroyed(this._destroyRef), delay(50))
      .subscribe((v) => {
        if (this.taskService.selectedTaskId()) {
          if (v === TaskDetailTargetPanel.Attachments) {
            const attachmentPanelElRef = this.attachmentPanelElRef();
            if (!attachmentPanelElRef) {
              devError('this.attachmentPanelElRef not ready');
              this._focusFirst();
            } else {
              this.focusItem(attachmentPanelElRef);
            }
          } else if (v === TaskDetailTargetPanel.Notes) {
            const noteWrapperElRef = this.noteWrapperElRef();
            // Focus the notes section in its rendered (preview) state — do NOT
            // also enter edit mode. This target opens via a checklist progress
            // badge or the "open notes" (N) shortcut; both should land on the
            // rendered notes, not the raw editor. Setting isFocusNotes opened
            // the textarea that focusItem() below immediately blurred back to
            // preview — a flash of raw "- [ ] " source (only visible for
            // checklists). Preview was always the settled state; explicit edits
            // still work via click/Enter (editActionTriggered).
            if (!noteWrapperElRef) {
              devError('this.noteWrapperElRef not ready');
              this._focusFirst();
            } else {
              this.focusItem(noteWrapperElRef);
            }
          } else {
            this._focusFirst();
          }
        }
      });
    Log.verbose('Task Detail Panel', this.task());
  }

  ngOnDestroy(): void {
    if (window.history.state?.[HISTORY_STATE.TASK_DETAIL_PANEL]) {
      window.history.back();
    }
    window.clearTimeout(this._focusTimeout);
  }

  changeTaskNotes($event: string): void {
    const defaultNotes = this.defaultTaskNotes();
    if (!defaultNotes || !$event || $event.trim() !== defaultNotes.trim()) {
      this.taskService.update(this.task().id, { notes: $event });
    }
  }

  estimateTime(): void {
    this._matDialog.open(DialogTimeEstimateComponent, {
      data: { task: this.task() },
    });
  }

  scheduleTask(): void {
    this._matDialog.open(DialogScheduleTaskComponent, {
      autoFocus: false,
      restoreFocus: true,
      data: { task: this.task() },
    });
  }

  openDeadlineDialog(): void {
    this._matDialog.open(DialogDeadlineComponent, {
      autoFocus: false,
      restoreFocus: true,
      data: { task: this.task() },
    });
  }

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

  editTaskRepeatCfg(): void {
    this._matDialog.open(DialogEditTaskRepeatCfgComponent, {
      restoreFocus: true,
      data: {
        task: this.task(),
        targetDate: this.task().dueDay || getDbDateStr(new Date(this.task().created)),
      },
    });
  }

  addAttachment(): void {
    this._matDialog
      .open(DialogEditTaskAttachmentComponent, {
        data: {},
      })
      .afterClosed()
      .subscribe((result) => {
        if (result) {
          this.attachmentService.addAttachment(this.task().id, {
            ...result,
          });
        }
      });
  }

  addSubTask(): void {
    const task = this.task();
    // The sub-task section (and thus the inline input) only renders for a
    // top-level task. On a subtask's own panel "add subtask" means "add a
    // sibling under my parent" — there is no section to host the input, so
    // create it directly (matches the pre-inline-draft behaviour).
    if (task.parentId) {
      this.taskService.addSubTaskTo(task.parentId);
      return;
    }

    if (task._hideSubTasksMode === HideSubTasksMode.HideAll) {
      this.taskService.showSubTasks(task.id);
    }
    const wasExpanded = this.isSubTasksExpanded();
    this.isSubTasksExpanded.set(true);
    this.isAddSubtaskInputVisible.set(true);
    // When already expanded the input is immediately focusable. When we just
    // expanded it, the panel body is visibility:hidden until the expand
    // animation finishes — onSubTasksAfterExpand() handles focus in that case.
    if (wasExpanded) {
      window.setTimeout(() => this.addSubtaskInput()?.focus());
    }
  }

  onSubTasksAfterExpand(): void {
    // Defer focus: with animations disabled Material fires afterExpand
    // synchronously inside the same change-detection pass, before the
    // addSubtaskInput viewChild is committed (it would be undefined here).
    if (this.isAddSubtaskInputVisible()) {
      window.setTimeout(() => this.addSubtaskInput()?.focus());
    }
  }

  onAddSubtaskInputClosed(reason: AddSubtaskInputCloseReason): void {
    this.isAddSubtaskInputVisible.set(false);
    // Keep the sub-task section expanded so the just-added sub-tasks stay
    // visible. On Escape (a keyboard cancel) return focus to the trigger so
    // keyboard navigation continues from the panel rather than falling to body.
    if (reason === 'escape') {
      window.setTimeout(() => this.addSubTaskBtn()?.nativeElement.focus());
    }
  }

  collapseParent(): void {
    if (!this.isDialogMode()) {
      this.taskService.setSelectedId(null);
      // NOTE: we delay for a frame to avoid problems with the global task keyboard shortcut handler
      window.setTimeout(() => {
        this.taskService.focusTaskIfPossible(this.task().id);
      });
    }
  }

  editCompleted(): void {
    const dialogRef = this._matDialog.open(DialogSelectDateTimeComponent, {
      data: {
        dateTime: this.task().doneOn,
      },
    });

    dialogRef.afterClosed().subscribe((doneOn) => {
      if (typeof doneOn === 'number') {
        this.taskService.update(this.task().id, { doneOn });
      }
    });
  }

  editCreated(): void {
    const dialogRef = this._matDialog.open(DialogSelectDateTimeComponent, {
      data: {
        dateTime: this.task().created,
      },
    });

    dialogRef.afterClosed().subscribe((created) => {
      if (typeof created === 'number') {
        this.taskService.update(this.task().id, { created });
      }
    });
  }

  onItemKeyPress(ev: KeyboardEvent): void {
    const itemEls = this.itemEls();
    if (!itemEls) {
      throw new Error();
    }

    const currentIndex = this.panelState.selectedItemIndex();
    if (ev.key === 'ArrowUp' && currentIndex > 0) {
      this.panelState.selectedItemIndex.set(currentIndex - 1);
      itemEls[currentIndex - 1].focusEl();
    } else if (ev.key === 'ArrowDown' && itemEls.length > currentIndex + 1) {
      this.panelState.selectedItemIndex.set(currentIndex + 1);
      itemEls[currentIndex + 1].focusEl();
    }
  }

  focusItem(cmpInstance: TaskDetailItemComponent, timeoutDuration: number = 150): void {
    this._scheduleTaskGuardedFocus(timeoutDuration, () => {
      const itemEls = this.itemEls();
      if (!itemEls) {
        throw new Error();
      }

      const i = itemEls.findIndex((el) => el === cmpInstance);
      if (i === -1) {
        this.focusItem(cmpInstance);
      } else {
        this.panelState.selectedItemIndex.set(i);
        cmpInstance.elementRef.nativeElement.focus();
      }
    });
  }

  /**
   * Schedules a deferred focus action, but only runs it if the panel still
   * shows the same task when the timer fires. A focus scheduled for one task
   * (e.g. the auto-focus on panel open) must not steal focus once the user has
   * navigated the list to a different task (#6578). Capturing the id at
   * schedule time — not at fire time — is what makes a late timer a no-op.
   */
  private _scheduleTaskGuardedFocus(delayMs: number, focusFn: () => void): void {
    window.clearTimeout(this._focusTimeout);
    const scheduledForTaskId = this.task().id;
    this._focusTimeout = window.setTimeout(() => {
      // Never steal focus from an open inline "add subtask" draft. The panel's
      // on-open auto-focus runs behind delay(50) + 150ms timers; under load
      // those can fire *after* the user already opened the draft. Focusing a
      // panel item then blurs the draft input, whose blur handler closes the
      // draft — leaving "Add subtask" silently broken (#8617/#8630).
      if (this.isAddSubtaskInputVisible()) {
        return;
      }
      if (this.task().id === scheduledForTaskId) {
        focusFn();
      }
    }, delayMs);
  }

  updateTaskTitleIfChanged(isChanged: boolean, newTitle: string): void {
    if (isChanged) {
      this.taskService.update(this.task().id, { title: newTitle });
    }
  }

  private _focusFirst(): void {
    this._scheduleTaskGuardedFocus(150, () => {
      const itemEls = this.itemEls();
      if (!itemEls) {
        throw new Error('No items found');
      }
      if (itemEls.length && itemEls[0]) {
        this.focusItem(itemEls[0], 0);
      }
    });
  }
}
