import { PersistentActionMeta } from '../../../op-log/core/persistent-action.interface';
import {
  __updateMultipleTaskSimple,
  addSubTask,
  moveSubTask,
  moveSubTaskDown,
  moveSubTaskToBottom,
  moveSubTaskToTop,
  moveSubTaskUp,
  removeTimeSpent,
  roundTimeSpentForDay,
  setCurrentTask,
  setSelectedTask,
  toggleStart,
  unsetCurrentTask,
  updateTaskUi,
} from './task.actions';
import { Task, TaskDetailTargetPanel, TaskState } from '../task.model';
import { calcTotalTimeSpent } from '../util/calc-total-time-spent';
import { addTaskRepeatCfgToTask } from '../../task-repeat-cfg/store/task-repeat-cfg.actions';
import {
  deleteTaskHelper,
  getTaskById,
  reCalcTimesForParentIfParent,
  reCalcTimeSpentForParentIfParent,
  updateTimeSpentForTask,
} from './task.reducer.util';
import { taskAdapter } from './task.adapter';
import { moveItemAfterAnchor } from '../../work-context/store/work-context-meta.helper';
import {
  arrayMoveLeft,
  arrayMoveRight,
  arrayMoveToEnd,
  arrayMoveToStart,
} from '../../../util/array-move';
import { filterOutId } from '../../../util/filter-out-id';
import {
  addTaskAttachment,
  deleteTaskAttachment,
  updateTaskAttachment,
} from '../task-attachment/task-attachment.actions';
import { Update } from '@ngrx/entity';
import { unique } from '../../../util/unique';
import { roundDurationVanilla } from '../../../util/round-duration';
import { loadAllData } from '../../../root-store/meta/load-all-data.action';
import { createReducer, on } from '@ngrx/store';
import { PlannerActions } from '../../planner/store/planner.actions';
import { TaskSharedActions } from '../../../root-store/meta/task-shared.actions';
import {
  TimeTrackingActions,
  syncTimeSpent,
} from '../../time-tracking/store/time-tracking.actions';
import { TaskLog } from '../../../core/log';
import { devError } from '../../../util/dev-error';

export { taskAdapter };

export const TASK_FEATURE_NAME = 'tasks';

/**
 * Checks if moving a task under a new parent would create a circular reference.
 * A circular reference occurs when taskId would become a subtask of one of its descendants.
 */
const wouldCreateCircularReference = (
  state: TaskState,
  taskId: string,
  newParentId: string,
): boolean => {
  // Check if newParentId is taskId itself or a descendant of taskId
  let current = newParentId;
  const visited = new Set<string>();

  while (current) {
    if (current === taskId) return true; // Would create cycle
    if (visited.has(current)) return false; // Already visited, no cycle
    visited.add(current);
    const task = state.entities[current];
    current = task?.parentId || '';
  }
  return false;
};

// Normalize timeSpentOnDay at the data boundary so all consumers can trust the
// invariant: timeSpentOnDay is always a valid object, never undefined. This mirrors
// normalizeCountOnDay in simple-counter.reducer.ts. Fixing it here (rather than
// adding optional-chaining guards at every access site) is correct because:
//   1. The TypeScript type says timeSpentOnDay is required — the compiler won't catch
//      unguarded accesses, so individual guards are invisible and get reverted.
//   2. The undefined comes from legacy persisted data written before the field existed.
//      It is a data-integrity issue, not a valid domain state; the type is correct.
//   3. Normalizing once at load time is cheaper and more reliable than N guards.
const normalizeTimeSpentOnDay = (state: TaskState): TaskState => {
  let hasUndefined = false;
  for (const id of state.ids as string[]) {
    if (state.entities[id] && !state.entities[id]!.timeSpentOnDay) {
      hasUndefined = true;
      break;
    }
  }
  if (!hasUndefined) return state;
  const entities: TaskState['entities'] = {};
  for (const id of state.ids as string[]) {
    const task = state.entities[id];
    entities[id] = task && !task.timeSpentOnDay ? { ...task, timeSpentOnDay: {} } : task;
  }
  return { ...state, entities };
};

const normalizeSubTaskIds = (state: TaskState): TaskState => {
  const parentIdsWithDuplicates: string[] = [];
  for (const id of state.ids as string[]) {
    const task = state.entities[id];
    if (task?.subTaskIds && new Set(task.subTaskIds).size !== task.subTaskIds.length) {
      parentIdsWithDuplicates.push(id);
    }
  }
  if (parentIdsWithDuplicates.length === 0) return state;

  const entities: TaskState['entities'] = { ...state.entities };
  for (const id of parentIdsWithDuplicates) {
    const task = state.entities[id];
    if (task) {
      entities[id] = {
        ...task,
        subTaskIds: unique(task.subTaskIds),
      };
    }
  }

  return { ...state, entities };
};

const reorderSubTask = (
  state: TaskState,
  id: string,
  parentId: string,
  moveFn: (subTaskIds: string[], id: string) => string[],
): TaskState => {
  const parentTask = state.entities[parentId];
  if (!parentTask) {
    TaskLog.err(`Parent task ${parentId} not found`);
    return state;
  }
  const parentSubTaskIds = parentTask.subTaskIds;

  // Check if the subtask is actually in the parent's subtask list
  if (!parentSubTaskIds.includes(id)) {
    TaskLog.err(`Subtask ${id} not found in parent ${parentId} subtasks`);
    return state;
  }

  return taskAdapter.updateOne(
    {
      id: parentId,
      changes: {
        subTaskIds: moveFn(parentSubTaskIds, id),
      },
    },
    state,
  );
};

// REDUCER
// -------
export const initialTaskState: TaskState = taskAdapter.getInitialState({
  // TODO maybe at least move those properties to an ui property
  currentTaskId: null,
  selectedTaskId: null,
  taskDetailTargetPanel: TaskDetailTargetPanel.Default,
  lastCurrentTaskId: null,
  isDataLoaded: false,
}) as TaskState;

export const taskReducer = createReducer<TaskState>(
  initialTaskState,

  // META ACTIONS
  // ------------
  on(loadAllData, (state, { appDataComplete }) => {
    if (!appDataComplete.task) return state;
    const task = appDataComplete.task;
    // Sanitize: ensure ids only contains IDs that have entities
    const ids = task.ids as string[];
    const hasOrphans = ids.some((id) => !task.entities[id]);
    if (hasOrphans) {
      devError('loadAllData: Found orphaned task IDs in loaded state — sanitizing');
    }
    const sanitized = hasOrphans
      ? { ...task, ids: ids.filter((id) => !!task.entities[id]) }
      : task;
    return normalizeSubTaskIds(
      normalizeTimeSpentOnDay({
        ...sanitized,
        currentTaskId: null,
        selectedTaskId: null,
        lastCurrentTaskId: task.currentTaskId,
        isDataLoaded: true,
      } as TaskState),
    );
  }),

  on(TaskSharedActions.deleteProject, (state, { allTaskIds }) => {
    return taskAdapter.removeMany(allTaskIds, {
      ...state,
      currentTaskId:
        state.currentTaskId && allTaskIds.includes(state.currentTaskId)
          ? null
          : state.currentTaskId,
    });
  }),

  on(TimeTrackingActions.addTimeSpent, (state, { task, date, duration }) => {
    // NaN/Infinity → JSON.stringify → null → auto-fix zeroes the day. Catch source.
    if (!Number.isFinite(duration)) {
      devError(
        `addTimeSpent: non-finite duration for task ${task.id} on ${date}: ${duration}`,
      );
      return state;
    }
    const currentTimeSpentForTickDay =
      (task.timeSpentOnDay && +task.timeSpentOnDay[date]) || 0;
    return updateTimeSpentForTask(
      task.id,
      {
        ...task.timeSpentOnDay,
        [date]: currentTimeSpentForTickDay + duration,
      },
      state,
    );
  }),

  // Sync time spent from remote clients
  // Local: no-op (state already updated by addTimeSpent ticks)
  // Remote: apply the batched duration
  on(syncTimeSpent, (state, action) => {
    // Only apply for remote actions - local state is already up-to-date
    if (!(action.meta as PersistentActionMeta).isRemote) {
      return state;
    }

    const { taskId, date, duration } = action;
    const task = state.entities[taskId];
    if (!task) {
      TaskLog.warn(`[syncTimeSpent] Task ${taskId} not found, skipping`);
      return state;
    }
    if (!Number.isFinite(duration)) {
      devError(
        `syncTimeSpent (remote): non-finite duration for task ${taskId} on ${date}: ${duration}`,
      );
      return state;
    }

    const currentTimeSpentForDay =
      (task.timeSpentOnDay && +task.timeSpentOnDay[date]) || 0;
    return updateTimeSpentForTask(
      taskId,
      {
        ...task.timeSpentOnDay,
        [date]: currentTimeSpentForDay + duration,
      },
      state,
    );
  }),

  //--------------------------------

  // TODO check if working
  on(setCurrentTask, (state, { id }) => {
    if (id) {
      const task = getTaskById(id, state);
      const subTaskIds = task.subTaskIds;
      let taskToStartId = id;
      if (subTaskIds && subTaskIds.length) {
        const undoneTasks = subTaskIds
          .map((tid) => getTaskById(tid, state))
          .filter((ta: Task) => !ta.isDone);
        taskToStartId = undoneTasks.length ? undoneTasks[0].id : subTaskIds[0];
      }
      return {
        ...taskAdapter.updateOne(
          {
            id: taskToStartId,
            changes: { isDone: false, doneOn: undefined },
          },
          state,
        ),
        currentTaskId: taskToStartId,
        selectedTaskId: state.selectedTaskId && taskToStartId,
      };
    } else {
      return {
        ...state,
        currentTaskId: null,
      };
    }
  }),

  on(unsetCurrentTask, (state) => {
    // Preserve lastCurrentTaskId when currentTaskId is already null (no-op unset).
    return {
      ...state,
      currentTaskId: null,
      lastCurrentTaskId: state.currentTaskId ?? state.lastCurrentTaskId,
    };
  }),

  on(setSelectedTask, (state, { id, taskDetailTargetPanel, isSkipToggle }) => {
    if (
      state.taskDetailTargetPanel === TaskDetailTargetPanel.DONT_OPEN_PANEL &&
      taskDetailTargetPanel !== null &&
      id
    ) {
      return {
        ...state,
        taskDetailTargetPanel: TaskDetailTargetPanel.DONT_OPEN_PANEL,
        selectedTaskId: id,
      };
    }

    if (isSkipToggle) {
      return {
        ...state,
        taskDetailTargetPanel: taskDetailTargetPanel || null,
        selectedTaskId: id,
      };
    }

    const isExplicitTargetPanel =
      !!taskDetailTargetPanel &&
      taskDetailTargetPanel !== TaskDetailTargetPanel.Default &&
      taskDetailTargetPanel !== TaskDetailTargetPanel.DONT_OPEN_PANEL;

    if (id && id === state.selectedTaskId && isExplicitTargetPanel) {
      return {
        ...state,
        taskDetailTargetPanel,
      };
    }

    return {
      ...state,
      taskDetailTargetPanel:
        !id || id === state.selectedTaskId ? null : taskDetailTargetPanel || null,
      selectedTaskId: id === state.selectedTaskId ? null : id,
    };
  }),

  // Task Actions
  // ------------
  on(__updateMultipleTaskSimple, (state, { taskUpdates }) => {
    return taskAdapter.updateMany(taskUpdates, state);
  }),

  on(updateTaskUi, (state, { task }) => {
    return taskAdapter.updateOne(task, state);
  }),

  // Bulk task updates - used for archive task batch operations
  on(TaskSharedActions.updateTasks, (state, { tasks }) => {
    return taskAdapter.updateMany(tasks, state);
  }),

  on(moveSubTask, (state, { taskId, srcTaskId, targetTaskId, afterTaskId }) => {
    // Guard against invalid moves (e.g. 'UNDONE' passed as ID) that may
    // appear in older op-log entries. Also reject self-moves where the
    // task being moved is its own src/target — that would put a task into
    // its own subTaskIds.
    if (
      !state.entities[srcTaskId] ||
      !state.entities[targetTaskId] ||
      taskId === srcTaskId ||
      taskId === targetTaskId
    ) {
      TaskLog.warn('Ignoring invalid moveSubTask action', {
        taskId,
        srcTaskId,
        targetTaskId,
      });
      return state;
    }

    // Prevent circular references (task becoming subtask of its own descendant)
    if (wouldCreateCircularReference(state, taskId, targetTaskId)) {
      TaskLog.err(
        `Cannot move task ${taskId} under ${targetTaskId}: would create circular reference`,
      );
      return state;
    }

    let newState = state;
    const oldPar = getTaskById(srcTaskId, state);
    const newPar = getTaskById(targetTaskId, state);

    // for old parent remove
    newState = taskAdapter.updateOne(
      {
        id: oldPar.id,
        changes: {
          subTaskIds: oldPar.subTaskIds.filter(filterOutId(taskId)),
        },
      },
      newState,
    );
    newState = reCalcTimesForParentIfParent(oldPar.id, newState);

    // for new parent add using anchor-based positioning
    const newParSubTaskIds = (newState.entities[newPar.id] as Task).subTaskIds;
    newState = taskAdapter.updateOne(
      {
        id: newPar.id,
        changes: {
          subTaskIds: moveItemAfterAnchor(taskId, afterTaskId, newParSubTaskIds),
        },
      },
      newState,
    );
    newState = reCalcTimesForParentIfParent(newPar.id, newState);

    // change parent id for moving task
    newState = taskAdapter.updateOne(
      {
        id: taskId,
        changes: {
          parentId: newPar.id,
          projectId: newPar.projectId,
        },
      },
      newState,
    );

    return newState;
  }),

  on(moveSubTaskUp, (state, { id, parentId }) =>
    reorderSubTask(state, id, parentId, arrayMoveLeft),
  ),

  on(moveSubTaskDown, (state, { id, parentId }) =>
    reorderSubTask(state, id, parentId, arrayMoveRight),
  ),

  on(moveSubTaskToTop, (state, { id, parentId }) =>
    reorderSubTask(state, id, parentId, arrayMoveToStart),
  ),

  on(moveSubTaskToBottom, (state, { id, parentId }) =>
    reorderSubTask(state, id, parentId, arrayMoveToEnd),
  ),

  on(removeTimeSpent, (state, { id, date, duration }) => {
    const task = getTaskById(id, state);
    const currentTimeSpentForTickDay =
      (task.timeSpentOnDay && +task.timeSpentOnDay[date]) || 0;

    return updateTimeSpentForTask(
      id,
      {
        ...task.timeSpentOnDay,
        [date]: Math.max(currentTimeSpentForTickDay - duration, 0),
      },
      state,
    );
  }),

  on(addSubTask, (state, { task, parentId }) => {
    const parentTask = state.entities[parentId];
    if (!parentTask) {
      devError(`Parent task ${parentId} not found when trying to add sub task`);
      return state;
    }

    // add item1
    const stateCopy = taskAdapter.addOne(
      {
        ...task,
        // update timeSpent if first sub task and non present
        // Guard timeSpentOnDay: legacy/imported tasks may have undefined here
        ...(parentTask.subTaskIds.length === 0 &&
        Object.keys(task.timeSpentOnDay || {}).length === 0
          ? {
              timeSpentOnDay: parentTask.timeSpentOnDay,
              timeSpent: calcTotalTimeSpent(parentTask.timeSpentOnDay),
            }
          : {}),
        // update timeEstimate if first sub task and non present
        ...(parentTask.subTaskIds.length === 0 && !task.timeEstimate
          ? { timeEstimate: parentTask.timeEstimate }
          : {}),
        parentId,
        // should always be empty
        tagIds: [],
        // should always be the one of the parent
        projectId: parentTask.projectId,
      },
      state,
    );

    // Keep this guard at the reducer boundary: normal creators generate fresh IDs,
    // but op-log replay/import or direct action dispatch can replay the same task ID.
    const parentSubTaskIds = parentTask.subTaskIds.includes(task.id)
      ? parentTask.subTaskIds
      : [...parentTask.subTaskIds, task.id];

    const stateWithParentTask: TaskState = {
      ...stateCopy,
      // update current task to new sub task if parent was current before
      ...(state.currentTaskId === parentId ? { currentTaskId: task.id } : {}),
      // also add to parent task
      entities: {
        ...stateCopy.entities,
        [parentId]: {
          ...parentTask,
          subTaskIds: parentSubTaskIds,
        },
      },
    };

    return reCalcTimesForParentIfParent(parentId, stateWithParentTask);
  }),

  on(toggleStart, (state) => {
    if (state.currentTaskId) {
      return {
        ...state,
        lastCurrentTaskId: state.currentTaskId,
      };
    }
    return state;
  }),

  on(roundTimeSpentForDay, (state, { day, taskIds, isRoundUp, roundTo, projectId }) => {
    const isLimitToProject: boolean = !!projectId || projectId === null;

    const idsToUpdateDirectly: string[] = taskIds.filter((id) => {
      const task: Task = getTaskById(id, state);
      return (
        (task.subTaskIds.length === 0 || !!task.parentId) &&
        (!isLimitToProject || task.projectId === projectId)
      );
    });
    const subTaskIds: string[] = idsToUpdateDirectly.filter(
      (id) => !!getTaskById(id, state).parentId,
    );
    const parentTaskToReCalcIds: string[] = unique<string>(
      subTaskIds.map((id) => getTaskById(id, state).parentId as string),
    );

    const updateSubsAndMainWithoutSubs: Update<Task>[] = idsToUpdateDirectly.map((id) => {
      // Guard timeSpentOnDay: legacy/imported tasks may have undefined here
      const spentOnDayBefore = getTaskById(id, state).timeSpentOnDay || {};
      const timeSpentOnDayUpdated = {
        ...spentOnDayBefore,
        [day]: roundDurationVanilla(spentOnDayBefore[day], roundTo, isRoundUp),
      };
      return {
        id,
        changes: {
          timeSpentOnDay: timeSpentOnDayUpdated,
          timeSpent: calcTotalTimeSpent(timeSpentOnDayUpdated),
        },
      };
    });

    // // update subs
    const newState = taskAdapter.updateMany(updateSubsAndMainWithoutSubs, state);
    // reCalc parents
    return parentTaskToReCalcIds.reduce(
      (acc, parentId) => reCalcTimeSpentForParentIfParent(parentId, acc),
      newState,
    );
  }),

  // TAG STUFF
  // ---------
  on(TaskSharedActions.addTagToTask, (state, { taskId, tagId }) => {
    const task = state.entities[taskId];
    if (!task) return state;
    return taskAdapter.updateOne(
      {
        id: taskId,
        changes: {
          tagIds: unique([...task.tagIds, tagId]),
        },
      },
      state,
    );
  }),

  // TASK ARCHIVE STUFF
  // ------------------
  on(TaskSharedActions.moveToArchive, (state, { tasks }) => {
    let copyState = state;
    tasks.forEach((task) => {
      copyState = deleteTaskHelper(copyState, task);
    });
    return {
      ...copyState,
    };
  }),

  // REPEAT STUFF
  // ------------
  on(addTaskRepeatCfgToTask, (state, { taskRepeatCfg, taskId }) => {
    return taskAdapter.updateOne(
      {
        id: taskId,
        changes: {
          repeatCfgId: taskRepeatCfg.id,
        },
      },
      state,
    );
  }),

  // TASK ATTACHMENTS
  // ----------------
  on(addTaskAttachment, (state, { taskId, taskAttachment }) => {
    return taskAdapter.updateOne(
      {
        id: taskId,
        changes: {
          attachments: [...getTaskById(taskId, state).attachments, taskAttachment],
        },
      },
      state,
    );
  }),

  on(updateTaskAttachment, (state, { taskId, taskAttachment }) => {
    const attachments = getTaskById(taskId, state).attachments;
    const updatedAttachments = attachments.map((attachment) =>
      attachment.id === taskAttachment.id
        ? {
            ...attachment,
            ...taskAttachment.changes,
          }
        : attachment,
    );

    return taskAdapter.updateOne(
      {
        id: taskId,
        changes: {
          attachments: updatedAttachments,
        },
      },
      state,
    );
  }),

  on(deleteTaskAttachment, (state, { taskId, id }) => {
    return taskAdapter.updateOne(
      {
        id: taskId,
        changes: {
          attachments: getTaskById(taskId, state).attachments.filter(
            (at) => at.id !== id,
          ),
        },
      },
      state,
    );
  }),

  // PLANNER STUFF
  // --------------
  // NOTE: transferTask is now handled in planner-shared.reducer.ts
  on(PlannerActions.moveBeforeTask, (state, { toTaskId, fromTask }) => {
    const targetTask = state.entities[toTaskId] as Task;
    if (!targetTask) {
      return state;
    }

    return taskAdapter.updateOne(
      {
        id: fromTask.id,
        changes: {
          dueDay: targetTask.dueDay,
          dueWithTime: undefined,
        },
      },
      state,
    );
  }),

  on(PlannerActions.planTaskForDay, (state, { task, day }) => {
    return taskAdapter.updateOne(
      {
        id: task.id,
        changes: {
          dueDay: day,
          dueWithTime: undefined,
          remindAt: undefined,
        },
      },
      state,
    );
  }),

  on(TaskSharedActions.removeTasksFromTodayTag, (state, { taskIds }) => {
    const validTaskIds = taskIds.filter((id) => !!state.entities[id]);
    if (validTaskIds.length !== taskIds.length) {
      devError(
        `removeTasksFromTodayTag: ${taskIds.length - validTaskIds.length} orphan ID(s) filtered`,
      );
    }
    return {
      ...state,
      // we do this to maintain the order of tasks when they are moved to overdue
      ids: [...validTaskIds, ...state.ids.filter((id) => !taskIds.includes(id))],
    };
  }),

  // Same reordering for the non-persistent variant (#6992)
  on(TaskSharedActions.localRemoveOverdueFromToday, (state, { taskIds }) => {
    const validTaskIds = taskIds.filter((id) => !!state.entities[id]);
    return {
      ...state,
      ids: [...validTaskIds, ...state.ids.filter((id) => !taskIds.includes(id))],
    };
  }),
);
