import { createFeatureSelector, createSelector } from '@ngrx/store';
import * as fromPlanner from './planner.reducer';
import {
  selectMapOfAllTasksInActiveProjects,
  selectAllTasksInActiveProjects,
} from '../../tasks/store/task.selectors';
import {
  NoStartTimeRepeatProjection,
  PlannerDay,
  PlannerDayMap,
  ScheduleItemEvent,
  ScheduleItemRepeatProjection,
  ScheduleItemTask,
  ScheduleItemType,
} from '../planner.model';
import {
  isAllDayCalendarEvent,
  ScheduleFromCalendarEvent,
} from '../../schedule/schedule.model';
import { Task, TaskCopy, TaskWithDueDay, TaskWithDueTime } from '../../tasks/task.model';
import { TaskRepeatCfg } from '../../task-repeat-cfg/task-repeat-cfg.model';
import { getDateTimeFromClockString } from '../../../util/get-date-time-from-clock-string';
import { isValidSplitTime } from '../../../util/is-valid-split-time';
import { devError } from '../../../util/dev-error';
import { getTimeLeftForTask } from '../../../util/get-time-left-for-task';
import { getDbDateStr } from '../../../util/get-db-date-str';
import { ScheduleCalendarMapEntry } from '../../schedule/schedule.model';
import { dateStrToUtcDate } from '../../../util/date-str-to-utc-date';
import { calculateAvailableHours } from '../util/calculate-available-hours';
import { selectTimelineConfig } from '../../config/store/global-config.reducer';
import { ScheduleConfig } from '../../config/global-config.model';
import {
  selectStartOfNextDayDiffMs,
  selectTodayStr,
} from '../../../root-store/app-state/app-state.selectors';
import { isTodayWithOffset } from '../../../util/is-today.util';
import { selectTaskRepeatCfgsForExactDay } from '../../task-repeat-cfg/store/task-repeat-cfg.selectors';

export const selectPlannerState = createFeatureSelector<fromPlanner.PlannerState>(
  fromPlanner.plannerFeatureKey,
);

export const selectAllTasksDueToday = createSelector(
  selectTodayStr,
  selectStartOfNextDayDiffMs,
  selectAllTasksInActiveProjects,
  selectPlannerState,
  (
    todayStr,
    startOfNextDayDiffMs,
    activeTasks,
    plannerState,
  ): (TaskWithDueTime | TaskWithDueDay)[] => {
    // activeTasks excludes tasks from archived projects, so filter planner IDs through it.
    const plannedTodayIds = new Set(plannerState.days[todayStr] || []);
    const allDue: (TaskWithDueTime | TaskWithDueDay)[] = activeTasks.filter(
      (t): t is TaskWithDueDay => plannedTodayIds.has(t.id),
    );

    // Use Set for O(1) lookup
    const allDueIds = new Set(allDue.map((t) => t.id));

    for (const task of activeTasks) {
      const id = task.id;
      if (allDueIds.has(id)) continue;

      // Check if task is due today
      // Priority: dueWithTime takes precedence over dueDay (mutual exclusivity pattern)
      let isDueToday = false;
      if (task.dueWithTime) {
        isDueToday = isTodayWithOffset(task.dueWithTime, todayStr, startOfNextDayDiffMs);
      } else if (task.dueDay === todayStr) {
        isDueToday = true;
      }

      if (isDueToday) {
        allDue.push(task as TaskWithDueTime | TaskWithDueDay);
        allDueIds.add(id);
      }
    }
    return allDue;
  },
);

// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export const selectTasksForPlannerDay = (day: string) => {
  return createSelector(
    selectPlannerState,
    selectAllTasksInActiveProjects,
    (plannerState, activeTasks) => {
      const dayIds = new Set(plannerState.days[day] || []);
      return activeTasks.filter((t) => dayIds.has(t.id)) as TaskCopy[];
    },
  );
};

// Updated selectPlannerDays
export const selectPlannerDays = (
  dayDates: string[],
  taskRepeatCfgs: TaskRepeatCfg[],
  todayListTaskIds: string[],
  calendarEvents: ScheduleCalendarMapEntry[],
  allPlannedTasks: TaskWithDueTime[],
  todayStr: string,
  // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
) => {
  // Use Set for O(1) lookup instead of O(n) .includes() in filter
  const allPlannedIdSet = new Set(allPlannedTasks.map((t) => t.id));
  const unplannedTaskIdsToday = todayListTaskIds.filter((id) => !allPlannedIdSet.has(id));

  return createSelector(
    selectMapOfAllTasksInActiveProjects,
    selectPlannerState,
    selectTimelineConfig,
    selectStartOfNextDayDiffMs,
    (activeTasks, plannerState, scheduleConfig, startOfNextDayDiffMs): PlannerDay[] => {
      const allDatesWithData = Object.keys(plannerState.days);
      const dayDatesToUse = [
        ...dayDates,
        ...allDatesWithData
          .filter((d) => plannerState.days[d].length && !dayDates.includes(d))
          .sort((a, b) => a.localeCompare(b)),
      ];

      // Pre-compute deadline tasks grouped by day (O(N) once, then O(1) per day)
      const deadlineMap = groupDeadlineTasksByDay(
        activeTasks.values(),
        startOfNextDayDiffMs,
      );

      return dayDatesToUse.map((dayDate) =>
        getPlannerDay(
          dayDate,
          todayStr,
          activeTasks,
          plannerState,
          taskRepeatCfgs,
          allPlannedTasks,
          calendarEvents,
          unplannedTaskIdsToday,
          deadlineMap,
          scheduleConfig,
          startOfNextDayDiffMs,
        ),
      );
    },
  );
};

export const selectPlannerDayMap = createSelector(
  selectMapOfAllTasksInActiveProjects,
  selectPlannerState,
  (taskMap, plannerState): PlannerDayMap => {
    const map: PlannerDayMap = {};

    Object.keys(plannerState.days).forEach((dayDate) => {
      const tids = plannerState.days[dayDate] || [];
      map[dayDate] = tids.map((id) => taskMap.get(id) as TaskCopy).filter((t) => !!t);
    });

    return map;
  },
);

const getPlannerDay = (
  dayDate: string,
  todayStr: string,
  taskMap: Map<string, Task>,
  plannerState: any,
  taskRepeatCfgs: TaskRepeatCfg[],
  allPlannedTasks: TaskWithDueTime[],
  calendarEvents: ScheduleCalendarMapEntry[],
  unplannedTaskIdsToday: string[] | false,
  deadlineTasksByDay: Record<string, TaskCopy[]>,
  scheduleConfig?: ScheduleConfig,
  startOfNextDayDiffMs: number = 0,
): PlannerDay => {
  const isTodayI = dayDate === todayStr;
  const currentDayDate = dateStrToUtcDate(dayDate);
  const currentDayTimestamp = currentDayDate.getTime();
  const tIds =
    isTodayI && unplannedTaskIdsToday
      ? unplannedTaskIdsToday
      : plannerState.days[dayDate] || [];
  const normalTasks = tIds
    .map((id) => taskMap.get(id) as TaskCopy)
    .filter((t) => !!t)
    // Filter out tasks with dueDay in future if it is Today's column
    .filter((t) => !isTodayI || !t.dueDay || t.dueDay <= todayStr);

  const {
    repeatProjectionsForDay: allRepeatProjectionsForDay,
    noStartTimeRepeatProjections: allNoStartTimeRepeatProjections,
  } = getAllRepeatableTasksForDay(taskRepeatCfgs, currentDayTimestamp);

  const scheduledTaskItems = getScheduledTaskItems(
    allPlannedTasks,
    dayDate,
    startOfNextDayDiffMs,
  );

  // A recurring task can already have a real instance in this day, either in
  // normalTasks (untimed instance, or any non-Today column) or in
  // scheduledTaskItems (timed `dueWithTime` instance on Today — excluded from
  // normalTasks because allPlannedTasks owns it). Drop the projection for any
  // config that already has an instance here so the same recurring task is not
  // counted twice — once as the real (done-aware) task and once as a full-
  // estimate projection. The "Today" view only ever counts the real task, so
  // without this the two views disagree on remaining time for done recurring
  // tasks. See #8220, #8232.
  const coveredRepeatCfgIds = new Set<string>();
  for (const t of normalTasks) {
    if (t.repeatCfgId) coveredRepeatCfgIds.add(t.repeatCfgId);
  }
  for (const si of scheduledTaskItems) {
    if (si.task.repeatCfgId) coveredRepeatCfgIds.add(si.task.repeatCfgId);
  }
  const repeatProjectionsForDay = allRepeatProjectionsForDay.filter(
    (rp) => !coveredRepeatCfgIds.has(rp.repeatCfg.id),
  );
  const noStartTimeRepeatProjections = allNoStartTimeRepeatProjections.filter(
    (rp) => !coveredRepeatCfgIds.has(rp.repeatCfg.id),
  );
  const { timedEvents, allDayEvents } = getIcalEventsForDay(
    calendarEvents,
    dayDate,
    startOfNextDayDiffMs,
  );

  const deadlineTasks = deadlineTasksByDay[dayDate] || [];

  const timeEstimate = getAllTimeSpent(
    normalTasks,
    repeatProjectionsForDay,
    noStartTimeRepeatProjections,
    scheduledTaskItems,
  );

  // Add calendar event durations (CalDAV/ICAL events) to the total time estimate
  // Only count timed events, not all-day events (which use raw 24h duration)
  const calendarEventsTime = timedEvents.reduce(
    (acc, ev) => acc + (ev.end - ev.start),
    0,
  );

  const totalTimeEstimate = timeEstimate + calendarEventsTime;

  // Calculate available hours and progress percentage
  let availableHours;
  let progressPercentage;

  if (scheduleConfig && scheduleConfig.isWorkStartEndEnabled) {
    availableHours = calculateAvailableHours(dayDate, scheduleConfig);
    progressPercentage =
      availableHours > 0 ? (totalTimeEstimate / availableHours) * 100 : 0;
  }

  return {
    isToday: isTodayI,
    dayDate,
    timeLimit: 0,
    itemsTotal:
      normalTasks.length +
      noStartTimeRepeatProjections.length +
      repeatProjectionsForDay.length +
      scheduledTaskItems.length +
      allDayEvents.length,
    scheduledIItems: [
      ...repeatProjectionsForDay,
      ...timedEvents,
      ...scheduledTaskItems,
    ].sort((a, b) => a.start - b.start),
    tasks: normalTasks,
    deadlineTasks,
    noStartTimeRepeatProjections,
    allDayEvents,
    timeEstimate: totalTimeEstimate,
    availableHours,
    progressPercentage,
  };
};

const getAllTimeSpent = (
  normalTasks: TaskCopy[],
  taskRepeatProjections: ScheduleItemRepeatProjection[],
  noStartTimeRepeatProjections: NoStartTimeRepeatProjection[],
  plannedTaskProjections: ScheduleItemTask[],
): number => {
  return (
    normalTasks.reduce((acc, t) => acc + (t.isDone ? 0 : getTimeLeftForTask(t)), 0) +
    taskRepeatProjections.reduce((acc, rp) => acc + rp.end - rp.start, 0) +
    plannedTaskProjections.reduce((acc, si) => acc + si.end - si.start, 0) +
    noStartTimeRepeatProjections.reduce(
      (acc, rp) => acc + (rp.repeatCfg.defaultEstimate || 0),
      0,
    )
  );
};

const getAllRepeatableTasksForDay = (
  taskRepeatCfgs: TaskRepeatCfg[],
  currentDayTimestamp: number,
): {
  repeatProjectionsForDay: ScheduleItemRepeatProjection[];
  noStartTimeRepeatProjections: NoStartTimeRepeatProjection[];
} => {
  const repeatProjectionsForDay: ScheduleItemRepeatProjection[] = [];
  const noStartTimeRepeatProjections: NoStartTimeRepeatProjection[] = [];
  const allRepeatableTasksForDay = selectTaskRepeatCfgsForExactDay.projector(
    taskRepeatCfgs,
    {
      dayDate: currentDayTimestamp,
    },
  );

  allRepeatableTasksForDay.forEach((repeatCfg) => {
    if (repeatCfg.startTime && isValidSplitTime(repeatCfg.startTime)) {
      const start = getDateTimeFromClockString(repeatCfg.startTime, currentDayTimestamp);
      const end = start + (repeatCfg.defaultEstimate || 0);
      repeatProjectionsForDay.push({
        id: repeatCfg.id,
        type: ScheduleItemType.RepeatProjection,
        start,
        end,
        repeatCfg,
      });
    } else {
      if (repeatCfg.startTime) {
        devError('Planner: Invalid startTime on repeat config');
      }
      noStartTimeRepeatProjections.push({
        id: repeatCfg.id,
        repeatCfg,
      });
    }
  });

  return {
    repeatProjectionsForDay,
    noStartTimeRepeatProjections,
  };
};

const getScheduledTaskItems = (
  allPlannedTasks: TaskWithDueTime[],
  dayDate: string,
  startOfNextDayDiffMs: number = 0,
): ScheduleItemTask[] =>
  allPlannedTasks
    .filter(
      (task) =>
        getDbDateStr(new Date(task.dueWithTime - startOfNextDayDiffMs)) === dayDate,
    )
    .map((task) => {
      const start = task.dueWithTime;
      // Mirror normalTasks: a done task contributes 0 remaining time. Without
      // this, a done timed task still adds its full estimate to the day total
      // (and a done timed recurring task double-counted with its projection
      // before the dedup above). See #8232.
      const end = start + (task.isDone ? 0 : getTimeLeftForTask(task));
      return {
        id: task.id,
        type: ScheduleItemType.Task,
        start,
        end,
        task,
      };
    });

interface IcalEventsForDayResult {
  timedEvents: ScheduleItemEvent[];
  allDayEvents: ScheduleFromCalendarEvent[];
}

const getIcalEventsForDay = (
  calendarEvents: ScheduleCalendarMapEntry[],
  dayDate: string,
  startOfNextDayDiffMs: number = 0,
): IcalEventsForDayResult => {
  const timedEvents: ScheduleItemEvent[] = [];
  const allDayEvents: ScheduleFromCalendarEvent[] = [];

  calendarEvents.forEach((icalMapEntry) => {
    icalMapEntry.items.forEach((calEv) => {
      const start = calEv.start;
      if (getDbDateStr(new Date(start - startOfNextDayDiffMs)) === dayDate) {
        if (isAllDayCalendarEvent(calEv)) {
          // Some providers expose all-day events as 24h timed events.
          allDayEvents.push({ ...calEv, isAllDay: true });
        } else {
          // Timed events go to scheduled items
          const end = calEv.start + calEv.duration;
          timedEvents.push({
            id: calEv.id,
            type: ScheduleItemType.CalEvent,
            start,
            end,
            calendarEvent: {
              ...calEv,
            },
          });
        }
      }
    });
  });
  return { timedEvents, allDayEvents };
};

/**
 * Groups all undone deadline tasks by their effective day string.
 * O(N) single pass — callers can then do O(1) map lookups per day.
 */
const groupDeadlineTasksByDay = (
  activeTasks: Iterable<Task>,
  startOfNextDayDiffMs: number = 0,
): Record<string, TaskCopy[]> => {
  const result: Record<string, TaskCopy[]> = {};
  for (const task of activeTasks) {
    if (task.isDone) continue;

    let dayKey: string | undefined;
    if (task.deadlineWithTime) {
      dayKey = getDbDateStr(new Date(task.deadlineWithTime - startOfNextDayDiffMs));
    } else if (task.deadlineDay) {
      dayKey = task.deadlineDay;
    }
    if (dayKey) {
      (result[dayKey] ??= []).push(task as TaskCopy);
    }
  }
  return result;
};
