import { inject, Injectable } from '@angular/core';
import { createEffect } from '@ngrx/effects';
import {
  concatMap,
  debounceTime,
  distinctUntilChanged,
  filter,
  first,
  switchMap,
  withLatestFrom,
} from 'rxjs/operators';
import { combineLatest, EMPTY, of } from 'rxjs';
import { GlobalTrackingIntervalService } from '../../../core/global-tracking-interval/global-tracking-interval.service';
import { TaskSharedActions } from '../../../root-store/meta/task-shared.actions';
import { Action, Store } from '@ngrx/store';
import {
  selectAllTasks,
  selectOverdueTasksOnToday,
  selectTasksDueForDay,
  selectTasksWithDueTimeForRange,
} from './task.selectors';
import { SyncWrapperService } from '../../../imex/sync/sync-wrapper.service';
import { selectTodayTaskIds } from '../../work-context/store/work-context.selectors';
import { AddTasksForTomorrowService } from '../../add-tasks-for-tomorrow/add-tasks-for-tomorrow.service';
import { getDateRangeForDay } from '../../../util/get-date-range-for-day';
import {
  selectStartOfNextDayDiffMs,
  selectTodayStr,
} from '../../../root-store/app-state/app-state.selectors';
import { TaskLog } from '../../../core/log';
import { SyncTriggerService } from '../../../imex/sync/sync-trigger.service';
import { environment } from '../../../../environments/environment';
import { HydrationStateService } from '../../../op-log/apply/hydration-state.service';
import { waitForSyncWindow } from '../../../util/wait-for-sync-window.operator';
import { selectTodayTagTaskIds } from '../../tag/store/tag.reducer';
import { getDeadlineAutoPlanDecision } from '../util/get-deadline-auto-plan-fields';

export const getOverdueIdsInTodayOrder = (
  overdue: ReadonlyArray<{ id: string }>,
  todayTagTaskIds: readonly string[],
): string[] => {
  const overdueIds = new Set(overdue.map((task) => task.id));
  return todayTagTaskIds.filter((id) => overdueIds.has(id));
};

@Injectable()
export class TaskDueEffects {
  private _store$ = inject(Store);
  private _globalTrackingIntervalService = inject(GlobalTrackingIntervalService);
  private _syncWrapperService = inject(SyncWrapperService);
  private _addTasksForTomorrowService = inject(AddTasksForTomorrowService);
  private _syncTriggerService = inject(SyncTriggerService);
  private _hydrationState = inject(HydrationStateService);

  // NOTE: this gets a lot of interference from tagEffect.preventParentAndSubTaskInTodayList$:
  // Uses afterInitialSyncDoneStrict$ to ensure sync has completed before creating repeat tasks,
  // preventing duplicate repeat task instances across clients (fixes repeat task duplication bug).
  //
  // KNOWN GAP (#6230): This effect only fires on todayDateStr$ date changes (distinctUntilChanged).
  // Once it fires for today, there is no intra-day re-check. If addAllDueToday() misses a config
  // (e.g., due to browser timer throttling around midnight, or system sleep/resume edge cases),
  // repeat tasks won't appear until the next date change or app restart.
  // Investigation (2026-03): day-change trigger, sync buffering, and data loading guards all
  // verified correct in isolation via e2e tests (see git history for Tests A and B).
  // The exact real-world trigger remains unidentified.
  // See: e2e/tests/recurring/repeat-task-day-change-bug-6230.spec.ts
  createRepeatableTasksAndAddDueToday$ = createEffect(
    () => {
      return this._syncTriggerService.afterInitialSyncDoneStrict$.pipe(
        first(),
        switchMap(() =>
          // Keep listening for date changes throughout the app lifecycle
          this._globalTrackingIntervalService.todayDateStr$.pipe(
            distinctUntilChanged(),
            // Wait for sync window to end instead of dropping the emission (#6192)
            waitForSyncWindow(
              this._hydrationState,
              'TaskDueEffects:createRepeatableTasksAndAddDueToday$',
            ),
            switchMap((dateStr) => {
              TaskLog.log(
                '[TaskDueEffects] Date changed, processing tasks for:',
                dateStr,
              );
              return this._syncWrapperService.afterCurrentSyncDoneOrSyncDisabled$;
            }),
            // Add debounce to ensure sync has fully completed and status is updated
            debounceTime(1000),
            // Ensure we're not in the middle of another sync
            switchMap(() => this._syncWrapperService.afterCurrentSyncDoneOrSyncDisabled$),
            // NOTE we use concatMap since tap errors only show in console, but are not handled by global handler
            concatMap(() => {
              TaskLog.log('[TaskDueEffects] Triggering addAllDueToday after sync', {
                isInSyncWindow: this._hydrationState.isInSyncWindow(),
              });
              return this._addTasksForTomorrowService.addAllDueToday();
            }),
          ),
        ),
      );
    },
    {
      dispatch: false,
    },
  );

  // NOTE: this gets a lot of interference from tagEffect.preventParentAndSubTaskInTodayList$:
  removeOverdueFormToday$ = createEffect(() => {
    return this._syncTriggerService.afterInitialSyncDoneAndDataLoadedInitially$.pipe(
      first(),
      switchMap(() =>
        // Keep listening for date changes throughout the app lifecycle
        this._globalTrackingIntervalService.todayDateStr$.pipe(
          distinctUntilChanged(),
          // Wait for sync window to end instead of dropping the emission (#6192)
          waitForSyncWindow(
            this._hydrationState,
            'TaskDueEffects:removeOverdueFormToday$',
          ),
          switchMap((dateStr) => {
            TaskLog.log('[TaskDueEffects] Date changed, removing overdue for:', dateStr);
            return this._syncWrapperService.afterCurrentSyncDoneOrSyncDisabled$;
          }),
          // Add debounce to ensure sync has fully completed and status is updated
          debounceTime(1000),
          // Ensure we're not in the middle of another sync
          switchMap(() => this._syncWrapperService.afterCurrentSyncDoneOrSyncDisabled$),
          switchMap(() => this._store$.select(selectOverdueTasksOnToday).pipe(first())),
          filter((overdue) => !!overdue.length),
          // Intentional raw TODAY_TAG order read. The computed selectTodayTaskIds
          // excludes overdue tasks by design, so it cannot remove stale raw IDs.
          withLatestFrom(this._store$.select(selectTodayTagTaskIds)),
          // we do this to maintain the order of tasks
          switchMap(([overdue, todayTagTaskIds]) => {
            const overdueIds = getOverdueIdsInTodayOrder(overdue, todayTagTaskIds);
            if (overdueIds.length === 0) {
              return EMPTY;
            }
            TaskLog.log('[TaskDueEffects] Removing overdue tasks from today', {
              overdueCount: overdueIds.length,
            });
            // Use non-persistent action to avoid LWW conflicts with user
            // actions on other devices (#6992). Every device runs this effect
            // independently, so syncing the removal is redundant.
            return of(
              TaskSharedActions.localRemoveOverdueFromToday({
                taskIds: overdueIds,
              }),
            );
          }),
        ),
      ),
    );
  });

  // Defensive effect to ensure tasks due today are in TODAY tag
  ensureTasksDueTodayInTodayTag$ = createEffect(() => {
    return this._syncTriggerService.afterInitialSyncDoneAndDataLoadedInitially$.pipe(
      first(),
      switchMap(() =>
        // Keep listening for date changes throughout the app lifecycle
        this._globalTrackingIntervalService.todayDateStr$.pipe(
          distinctUntilChanged(),
          // Wait for sync window to end instead of dropping the emission (#6192)
          waitForSyncWindow(
            this._hydrationState,
            'TaskDueEffects:ensureTasksDueTodayInTodayTag$',
          ),
          switchMap((dateStr) => {
            TaskLog.log('[TaskDueEffects] Ensuring tasks for:', dateStr);
            return this._syncWrapperService.afterCurrentSyncDoneOrSyncDisabled$;
          }),
          debounceTime(2000), // Wait a bit longer to ensure all other effects have run
          switchMap(() => this._syncWrapperService.afterCurrentSyncDoneOrSyncDisabled$),
          withLatestFrom(
            this._store$.select(selectTodayStr),
            this._store$.select(selectStartOfNextDayDiffMs),
          ),
          switchMap(([, todayStr, startOfNextDayDiffMs]) => {
            // TODO(startOfNextDay): migrate to dateService.getLogicalTodayDate() once
            // the spec (task-due.effects.spec.ts) is un-xdescribe'd. Deferred from the
            // logical-clock centralization refactor because this effect is driven by
            // selectors (replay-sensitive — see CLAUDE.md note #8) and the test suite
            // can't verify the change.
            const todayRange = getDateRangeForDay(Date.now() - startOfNextDayDiffMs);
            return combineLatest([
              this._store$.select(selectTasksDueForDay, { day: todayStr }),
              this._store$.select(selectTasksWithDueTimeForRange, todayRange),
              this._store$.select(selectAllTasks),
            ]).pipe(
              first(),
              withLatestFrom(
                this._store$.select(selectTodayTaskIds),
                this._store$.select(selectTodayTagTaskIds),
              ),
              concatMap(
                ([
                  [tasksDueByDay, tasksDueByTime, allTasks],
                  todayTaskIds,
                  todayTagTaskIds,
                ]) => {
                  const context = { today: todayStr, startOfNextDayDiffMs };
                  const taskById = new Map(
                    allTasks.map((task) => [task.id, task] as const),
                  );

                  // Tasks with dueWithTime set have dueDay cleared (mutual exclusivity),
                  // so we must check both due selectors to catch all scheduled tasks.
                  const seenIds = new Set<string>();
                  const allTasksDueToday = [...tasksDueByDay, ...tasksDueByTime].filter(
                    (task) => {
                      if (seenIds.has(task.id)) return false;
                      seenIds.add(task.id);
                      return true;
                    },
                  );

                  const todayTaskIdSet = new Set(todayTaskIds);
                  const missingDueTaskIds = allTasksDueToday
                    .filter((task) => !todayTaskIdSet.has(task.id))
                    // Exclude subtasks whose parent is already in TODAY
                    // (preventParentAndSubTaskInTodayList$ will remove them anyway,
                    // causing an infinite add/remove loop and phantom sync changes)
                    .filter(
                      (task) => !task.parentId || !todayTaskIdSet.has(task.parentId),
                    )
                    .map((task) => task.id);

                  const plannedTodayIds = new Set([
                    ...todayTagTaskIds,
                    ...missingDueTaskIds,
                  ]);
                  const missingDeadlineTaskIds: string[] = [];

                  for (const task of [...allTasks].sort(
                    (a, b) => Number(!!a.parentId) - Number(!!b.parentId),
                  )) {
                    const parentTask = task.parentId
                      ? taskById.get(task.parentId)
                      : undefined;
                    const decision = getDeadlineAutoPlanDecision(
                      task,
                      context,
                      plannedTodayIds,
                      parentTask,
                    );

                    if (decision.shouldAutoPlan) {
                      missingDeadlineTaskIds.push(task.id);
                      plannedTodayIds.add(task.id);
                    }
                  }

                  // Debug log to investigate repeated operations
                  TaskLog.log('[TaskDueEffects] ensureTasksDueTodayInTodayTag check:', {
                    tasksDueByDayCount: tasksDueByDay.length,
                    tasksDueByTimeCount: tasksDueByTime.length,
                    tasksDueTodayIds: allTasksDueToday.map((t) => t.id),
                    todayTaskIdsCount: todayTaskIds.length,
                    todayTagTaskIdsCount: todayTagTaskIds.length,
                    missingDueTaskIds,
                    missingDeadlineTaskIds,
                    willDispatch:
                      missingDueTaskIds.length > 0 || missingDeadlineTaskIds.length > 0,
                  });

                  if (
                    !environment.production &&
                    (missingDueTaskIds.length > 0 || missingDeadlineTaskIds.length > 0)
                  ) {
                    TaskLog.err('[TaskDueEffects] Found tasks missing from TODAY tag:', {
                      tasksDueToday: allTasksDueToday.length,
                      todayTaskIds: todayTaskIds.length,
                      missingDueTaskIds,
                      missingDeadlineTaskIds,
                    });
                  }

                  const actions: Action[] = [];
                  if (missingDueTaskIds.length > 0) {
                    actions.push(
                      TaskSharedActions.planTasksForToday({
                        taskIds: missingDueTaskIds,
                        today: todayStr,
                        startOfNextDayDiffMs,
                        isSkipRemoveReminder: true,
                      }),
                    );
                  }

                  if (missingDeadlineTaskIds.length > 0) {
                    actions.push(
                      TaskSharedActions.planDeadlineTasksForToday({
                        taskIds: missingDeadlineTaskIds,
                        today: todayStr,
                        startOfNextDayDiffMs,
                      }),
                    );
                  }

                  return actions.length > 0 ? of(...actions) : EMPTY;
                },
              ),
            );
          }),
        ),
      ),
    );
  });
}
