"use client";

import {
  VacationStatus,
  type AllocationLike,
  type AllocationReadModel,
  type Assignment,
  type DemandRequirement,
} from "@capakraken/shared";
import { createContext, useContext, useEffect, useMemo, useRef, useState, type ReactNode } from "react";
import { useSession } from "next-auth/react";
import { useSearchParams } from "next/navigation";
import { useTimelineSSE } from "~/hooks/useTimelineSSE.js";
import { trpc } from "~/lib/trpc/client.js";
import { readAppPreferences, useAppPreferences } from "~/hooks/useAppPreferences.js";
import type { HeatmapColorScheme } from "~/hooks/useAppPreferences.js";

export type TimelineDisplayMode = "strip" | "bar" | "heatmap";
import { addDays } from "./utils.js";
import { DEFAULT_FILTERS, type TimelineFilters } from "./TimelineFilter.js";
import { DONE_STATUSES } from "./timelineConstants.js";
import { toLocalDateKey } from "./timelineAvailability.js";

// ─── Local timeline types ─────────────────────────────────────────────────────
// These re-declare the shapes that the original TimelineView used internally.
// Kept here so every timeline sub-component can import them from one place.

export type TimelineResource = {
  id: string;
  displayName: string;
  eid: string;
  chapter?: string | null;
  lcrCents: number;
  availability?: unknown;
};

export type TimelineProject = {
  id: string;
  name: string;
  shortCode: string;
  status: string;
  startDate: Date | string;
  endDate: Date | string;
  orderType: string;
  clientId?: string | null;
  budgetCents?: number;
  winProbability?: number;
  color?: string | null;
  staffingReqs?: unknown;
  responsiblePerson?: string | null;
};

export type TimelineRole = {
  id: string;
  name: string;
  color: string | null;
};

export type TimelineAllocation = Omit<
  AllocationLike,
  "resource" | "project" | "roleEntity" | "metadata"
> & {
  resource?: TimelineResource | null;
  project: TimelineProject;
  roleEntity?: TimelineRole | null;
  metadata: Record<string, unknown> | null;
};

export type TimelineAssignmentEntry = Assignment<TimelineAllocation>;
export type TimelineDemandEntry = DemandRequirement<TimelineAllocation>;

export type TimelineEntriesView = AllocationReadModel<TimelineAllocation> & {
  assignments: TimelineAssignmentEntry[];
  demands: TimelineDemandEntry[];
};
export type TimelineProjectEntry = TimelineAssignmentEntry | TimelineDemandEntry;

export type ViewMode = "resource" | "project";

function buildTimelineFiltersFromSearchParams(searchParams: ReturnType<typeof useSearchParams>): TimelineFilters {
  const savedPrefs = readAppPreferences();
  const next: TimelineFilters = {
    ...DEFAULT_FILTERS,
    hideCompletedProjects: savedPrefs.hideCompletedProjects,
    showPlaceholders: savedPrefs.showDemandProjects,
  };

  const eids = searchParams.get("eids");
  if (eids) next.eids = eids.split(",").filter(Boolean);
  const projectIds = searchParams.get("projectIds");
  if (projectIds) next.projectIds = projectIds.split(",").filter(Boolean);
  const chapters = searchParams.get("chapters");
  if (chapters) next.chapters = chapters.split(",").filter(Boolean);
  const clientIds = searchParams.get("clientIds");
  if (clientIds) next.clientIds = clientIds.split(",").filter(Boolean);
  const countryCodes = searchParams.get("countryCodes");
  if (countryCodes) next.countryCodes = countryCodes.split(",").filter(Boolean);

  if (eids || projectIds) {
    next.showDrafts = true;
    next.hideCompletedProjects = false;
  }

  return next;
}

// ─── Derived resource type used throughout the timeline ─────────────────────
export type ResourceBrief = {
  id: string;
  displayName: string;
  eid: string;
  chapter: string | null | undefined;
};

// ─── Project group type (project view) ──────────────────────────────────────
export type ProjectGroup = {
  id: string;
  name: string;
  shortCode: string;
  orderType: string;
  clientId: string | null;
  startDate: Date;
  endDate: Date;
  status: string;
  color?: string | null;
  resourceRows: { resource: ResourceBrief; allocs: TimelineAssignmentEntry[] }[];
};

// ─── Vacation entry type (inferred from tRPC) ──────────────────────────────
export type VacationEntry = {
  id: string;
  resourceId: string;
  type: string;
  startDate: Date | string;
  endDate: Date | string;
  note?: string | null;
  scope?: string | null;
  calendarName?: string | null;
  sourceType?: string | null;
  countryCode?: string | null;
  countryName?: string | null;
  federalState?: string | null;
  metroCityName?: string | null;
  status: string;
  requestedBy?: { name?: string | null; email: string } | null;
  approvedBy?: { name?: string | null; email: string } | null;
  approvedAt?: Date | string | null;
  isHalfDay?: boolean;
  halfDayPart?: string | null;
};

export type HolidayOverlayEntry = {
  id: string;
  resourceId: string;
  type: string;
  status: string;
  startDate: Date | string;
  endDate: Date | string;
  note?: string | null;
  scope?: string | null;
  calendarName?: string | null;
  sourceType?: string | null;
  countryCode?: string | null;
  countryName?: string | null;
  federalState?: string | null;
  metroCityName?: string | null;
};

// ─── Context shape ──────────────────────────────────────────────────────────

export interface TimelineContextValue {
  // ─ Data
  assignments: TimelineAssignmentEntry[];
  demands: TimelineDemandEntry[];
  visibleAssignments: TimelineAssignmentEntry[];
  visibleDemands: TimelineDemandEntry[];
  vacationsByResource: Map<string, VacationEntry[]>;
  resources: ResourceBrief[];
  resourceMap: Map<string, ResourceBrief>;
  allocsByResource: Map<string, TimelineAssignmentEntry[]>;
  projectGroups: ProjectGroup[];
  openDemandsByProject: Map<string, TimelineDemandEntry[]>;

  // ─ View state
  viewStart: Date;
  viewEnd: Date;
  viewDays: number;
  setViewStart: React.Dispatch<React.SetStateAction<Date>>;
  setViewDays: React.Dispatch<React.SetStateAction<number>>;
  filters: TimelineFilters;
  setFilters: React.Dispatch<React.SetStateAction<TimelineFilters>>;
  filterOpen: boolean;
  setFilterOpen: React.Dispatch<React.SetStateAction<boolean>>;
  viewMode: ViewMode;
  setViewMode: React.Dispatch<React.SetStateAction<ViewMode>>;
  today: Date;

  // ─ Display preferences
  displayMode: TimelineDisplayMode;
  heatmapScheme: HeatmapColorScheme;
  blinkOverbookedDays: boolean;

  // ─ Loading
  isLoading: boolean;
  isInitialLoading: boolean;
  isEntriesError: boolean;
  totalAllocCount: number;
  activeFilterCount: number;

  // ─ SSE is initialized by the provider (no value exposed)
}

const TimelineContext = createContext<TimelineContextValue | null>(null);

export function useTimelineContext(): TimelineContextValue {
  const ctx = useContext(TimelineContext);
  if (!ctx) {
    throw new Error("useTimelineContext must be used within a <TimelineProvider>");
  }
  return ctx;
}

// ─── Provider ───────────────────────────────────────────────────────────────

interface TimelineProviderProps {
  /** Cross-project context resource IDs from drag — injected from the parent. */
  contextAllocations: TimelineAssignmentEntry[];
  isDragging: boolean;
  children: ReactNode;
}

export function TimelineProvider({
  contextAllocations,
  isDragging,
  children,
}: TimelineProviderProps) {
  const { data: session, status: sessionStatus } = useSession();
  const searchParams = useSearchParams();
  const role = sessionStatus === "authenticated"
    ? ((session.user as { role?: string } | undefined)?.role ?? "USER")
    : null;
  const isSelfServiceTimeline = role === "USER" || role === "VIEWER";
  const isRoleLoading = sessionStatus === "loading";

  const today = useMemo(() => {
    const d = new Date();
    d.setHours(0, 0, 0, 0);
    return d;
  }, []);

  // Support URL params: ?startDate=2026-01-01&days=120
  const [viewStart, setViewStart] = useState(() => {
    const sp = searchParams.get("startDate");
    if (sp) {
      const d = new Date(sp);
      if (!isNaN(d.getTime())) return d;
    }
    return addDays(today, -30);
  });
  const [viewDays, setViewDays] = useState(() => {
    const sp = searchParams.get("days");
    if (sp) {
      const n = parseInt(sp, 10);
      if (n > 0 && n <= 365) return n;
    }
    return 180;
  });
  const viewEnd = addDays(viewStart, viewDays);

  // Support URL params: ?eids=EMP-001,EMP-002&projectIds=id1,id2&chapters=ch1
  const [filters, setFilters] = useState<TimelineFilters>(() => buildTimelineFiltersFromSearchParams(searchParams));

  // Sync filters/viewStart/viewDays from URL params on mount and after later changes
  // (e.g. direct nav from another page or router.push("/timeline?eids=...") while already on /timeline)
  useEffect(() => {
    const spStart = searchParams.get("startDate");
    setViewStart(() => {
      if (spStart) {
        const d = new Date(spStart);
        if (!isNaN(d.getTime())) return d;
      }
      return addDays(today, -30);
    });

    const spDays = searchParams.get("days");
    setViewDays(() => {
      if (spDays) {
        const n = parseInt(spDays, 10);
        if (n > 0 && n <= 365) return n;
      }
      return 180;
    });

    setFilters(buildTimelineFiltersFromSearchParams(searchParams));
  }, [searchParams, today]);

  const [filterOpen, setFilterOpen] = useState(false);
  const [viewMode, setViewMode] = useState<ViewMode>("resource");

  useTimelineSSE();

  const { prefs: appPrefs } = useAppPreferences();
  const displayMode = appPrefs.timelineDisplayMode;
  const heatmapScheme = appPrefs.heatmapColorScheme;
  const blinkOverbookedDays = appPrefs.blinkOverbookedDays;

  // ─── Data queries ──────────────────────────────────────────────────────────
  const initialRefreshKeyRef = useRef<string | null>(null);
  const timelineQueryInput = {
    startDate: viewStart,
    endDate: viewEnd,
    ...(filters.clientIds.length > 0 ? { clientIds: filters.clientIds } : {}),
    ...(filters.projectIds.length > 0 ? { projectIds: filters.projectIds } : {}),
    ...(filters.chapters.length > 0 ? { chapters: filters.chapters } : {}),
    ...(filters.eids.length > 0 ? { eids: filters.eids } : {}),
    ...(filters.countryCodes.length > 0 ? { countryCodes: filters.countryCodes } : {}),
  };

  const staffEntriesViewQuery = trpc.timeline.getEntriesView.useQuery(
    timelineQueryInput,
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    {
      enabled: !isRoleLoading && !isSelfServiceTimeline,
      placeholderData: (prev: any) => prev,
      refetchOnWindowFocus: false,
      staleTime: 90_000,
    },
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
  ) as {
    data: TimelineEntriesView | undefined;
    isLoading: boolean;
    isError: boolean;
    refetch: () => Promise<unknown>;
  };

  const selfEntriesViewQuery = trpc.timeline.getMyEntriesView.useQuery(
    timelineQueryInput,
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    {
      enabled: !isRoleLoading && isSelfServiceTimeline,
      placeholderData: (prev: any) => prev,
      refetchOnWindowFocus: false,
      staleTime: 90_000,
    },
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
  ) as {
    data: TimelineEntriesView | undefined;
    isLoading: boolean;
    isError: boolean;
    refetch: () => Promise<unknown>;
  };

  const entriesViewQuery = isSelfServiceTimeline ? selfEntriesViewQuery : staffEntriesViewQuery;
  const { data: entriesView, isLoading, isError: isEntriesError, refetch: refetchEntriesView } = entriesViewQuery;

  const assignments = entriesView?.assignments ?? [];
  const demands = entriesView?.demands ?? [];

  // Avoid TS deep-instantiation blow-ups on the large TRPC hook type here.
  const vacationListQuery = trpc.vacation.list.useQuery as unknown as (
    input: {
      startDate: Date;
      endDate: Date;
      status: VacationStatus[];
      limit: number;
    },
    options: {
      placeholderData: (prev: VacationEntry[] | undefined) => VacationEntry[] | undefined;
      refetchOnWindowFocus: boolean;
      staleTime: number;
    },
  ) => {
    data: VacationEntry[] | undefined;
    refetch: () => Promise<unknown>;
  };
  const vacationEntriesQuery = vacationListQuery(
    { startDate: viewStart, endDate: viewEnd, status: [VacationStatus.APPROVED, VacationStatus.PENDING], limit: 500 },
    { placeholderData: (prev) => prev, refetchOnWindowFocus: false, staleTime: 90_000 },
  );
  const {
    data: vacationEntries = [],
    refetch: refetchVacations,
  } = vacationEntriesQuery;

  const staffHolidayOverlayQuery = trpc.timeline.getHolidayOverlays.useQuery(
    timelineQueryInput,
    {
      enabled: !isRoleLoading && !isSelfServiceTimeline,
      placeholderData: (prev) => prev,
      refetchOnWindowFocus: false,
      staleTime: 90_000,
    },
  );
  const selfHolidayOverlayQuery = trpc.timeline.getMyHolidayOverlays.useQuery(
    timelineQueryInput,
    {
      enabled: !isRoleLoading && isSelfServiceTimeline,
      placeholderData: (prev) => prev,
      refetchOnWindowFocus: false,
      staleTime: 90_000,
    },
  );
  const activeHolidayOverlayQuery = isSelfServiceTimeline ? selfHolidayOverlayQuery : staffHolidayOverlayQuery;
  const {
    data: holidayOverlayEntries = [],
    refetch: refetchHolidayOverlays,
  } = activeHolidayOverlayQuery;

  const initialRefreshKey = useMemo(
    () =>
      JSON.stringify({
        isSelfServiceTimeline,
        start: viewStart.toISOString(),
        end: viewEnd.toISOString(),
        clients: filters.clientIds,
        projects: filters.projectIds,
        chapters: filters.chapters,
        eids: filters.eids,
        countries: filters.countryCodes,
      }),
    [
      filters.chapters,
      filters.clientIds,
      filters.countryCodes,
      filters.eids,
      filters.projectIds,
      isSelfServiceTimeline,
      viewEnd,
      viewStart,
    ],
  );

  useEffect(() => {
    if (isRoleLoading) return;
    if (initialRefreshKeyRef.current === initialRefreshKey) return;
    initialRefreshKeyRef.current = initialRefreshKey;

    // Harden client-side route and filter transitions: refresh the core
    // read models once per active timeline query context instead of trusting
    // prefetched or placeholder state to self-heal.
    void refetchEntriesView();
    void refetchVacations();
    void refetchHolidayOverlays();
  }, [
    initialRefreshKey,
    isRoleLoading,
    refetchEntriesView,
    refetchHolidayOverlays,
    refetchVacations,
  ]);

  const vacationsByResource = useMemo(() => {
    const map = new Map<string, VacationEntry[]>();
    const mergedEntries = [...(vacationEntries as VacationEntry[])];
    const existingKeys = new Set(
      mergedEntries.map((vacation) => {
        const start = toLocalDateKey(vacation.startDate);
        const end = toLocalDateKey(vacation.endDate);
        return `${vacation.resourceId}:${vacation.type}:${start}:${end}`;
      }),
    );

    for (const holiday of holidayOverlayEntries as HolidayOverlayEntry[]) {
      const start = toLocalDateKey(holiday.startDate);
      const end = toLocalDateKey(holiday.endDate);
      const key = `${holiday.resourceId}:${holiday.type}:${start}:${end}`;
      if (existingKeys.has(key)) {
        continue;
      }

      existingKeys.add(key);
      mergedEntries.push(holiday as VacationEntry);
    }

    for (const vacation of mergedEntries) {
      const existing = map.get(vacation.resourceId);
      if (existing) {
        existing.push(vacation);
      } else {
        map.set(vacation.resourceId, [vacation]);
      }
    }
    return map;
  }, [holidayOverlayEntries, vacationEntries]);

  // When EID filter is active, explicitly fetch those resources.
  const { data: eidFilterData } = trpc.resource.directory.useQuery(
    { eids: filters.eids, limit: 100 },
    { enabled: !isSelfServiceTimeline && filters.eids.length > 0, staleTime: 30_000 },
  );

  // ─── Filtered entries ──────────────────────────────────────────────────────

  const visibleAssignments = useMemo(
    () =>
      assignments.filter((entry) => {
        if (entry.project.status === "DRAFT" && !filters.showDrafts) return false;
        if (DONE_STATUSES.has(entry.project.status) && filters.hideCompletedProjects) return false;
        return true;
      }),
    [assignments, filters.hideCompletedProjects, filters.showDrafts],
  );

  const visibleDemands = useMemo(
    () =>
      demands.filter((entry) => {
        if (entry.project.status === "DRAFT" && !filters.showDrafts) return false;
        if (DONE_STATUSES.has(entry.project.status) && filters.hideCompletedProjects) return false;
        if (!filters.showPlaceholders) return false;
        // Hide fully-filled demands (status COMPLETED or unfilledHeadcount <= 0)
        const demandEntry = entry as { status?: string; unfilledHeadcount?: number };
        if (demandEntry.status === "COMPLETED") return false;
        if (typeof demandEntry.unfilledHeadcount === "number" && demandEntry.unfilledHeadcount <= 0) return false;
        return true;
      }),
    [demands, filters.hideCompletedProjects, filters.showDrafts, filters.showPlaceholders],
  );

  const openDemandsByProject = useMemo(() => {
    const map = new Map<string, TimelineDemandEntry[]>();
    for (const demand of visibleDemands) {
      const arr = map.get(demand.projectId) ?? [];
      arr.push(demand);
      map.set(demand.projectId, arr);
    }
    return map;
  }, [visibleDemands]);

  // ─── Resource map + allocsByResource ──────────────────────────────────────
  const { resourceMap, allocsByResource, resources } = useMemo(() => {
    const resourceMap = new Map<string, ResourceBrief>();
    const allocsByResource = new Map<string, TimelineAssignmentEntry[]>();
    const firstAssignmentByResource = new Map<string, TimelineAssignmentEntry>();
    const projectIdsByResource = new Map<string, Set<string>>();
    const clientIdsByResource = new Map<string, Set<string>>();
    const chapterFilter = new Set(filters.chapters);
    const eidFilter = new Set(filters.eids);
    const projectFilter = new Set(filters.projectIds);
    const clientFilter = new Set(filters.clientIds);

    if (eidFilterData?.resources) {
      for (const r of eidFilterData.resources as {
        id: string;
        displayName: string;
        eid: string;
        chapter: string | null;
      }[]) {
        if (!resourceMap.has(r.id)) {
          resourceMap.set(r.id, {
            id: r.id,
            displayName: r.displayName,
            eid: r.eid,
            chapter: r.chapter,
          });
        }
      }
    }

    for (const entry of visibleAssignments) {
      if (!entry.resourceId) continue;
      firstAssignmentByResource.set(entry.resourceId, entry);
      if (!resourceMap.has(entry.resourceId)) {
        resourceMap.set(entry.resourceId, {
          id: entry.resource!.id,
          displayName: entry.resource!.displayName,
          eid: entry.resource!.eid,
          chapter: entry.resource!.chapter,
        });
      }
      const arr = allocsByResource.get(entry.resourceId) ?? [];
      arr.push(entry);
      allocsByResource.set(entry.resourceId, arr);

      const projectIds = projectIdsByResource.get(entry.resourceId) ?? new Set<string>();
      projectIds.add(entry.projectId);
      projectIdsByResource.set(entry.resourceId, projectIds);

      if (typeof entry.project.clientId === "string") {
        const clientIds = clientIdsByResource.get(entry.resourceId) ?? new Set<string>();
        clientIds.add(entry.project.clientId);
        clientIdsByResource.set(entry.resourceId, clientIds);
      }
    }

    // Merge cross-project context allocations so they appear during drag
    if (isDragging && contextAllocations.length > 0) {
      for (const ca of contextAllocations) {
        if (!ca.resourceId) continue;
        const existing = firstAssignmentByResource.get(ca.resourceId);
        if (existing && !resourceMap.has(ca.resourceId)) {
          resourceMap.set(ca.resourceId, {
            id: existing.resource!.id,
            displayName: existing.resource!.displayName,
            eid: existing.resource!.eid,
            chapter: existing.resource!.chapter,
          });
        }
      }
    }

    let resources = [...resourceMap.values()].sort((a, b) =>
      a.displayName.localeCompare(b.displayName),
    );
    if (chapterFilter.size > 0) {
      resources = resources.filter((r) => r.chapter && chapterFilter.has(r.chapter));
    }
    if (eidFilter.size > 0) {
      resources = resources.filter((r) => eidFilter.has(r.eid));
    }
    if (projectFilter.size > 0) {
      resources = resources.filter((r) => {
        const projectIds = projectIdsByResource.get(r.id);
        if (!projectIds) return false;
        for (const projectId of projectIds) {
          if (projectFilter.has(projectId)) {
            return true;
          }
        }
        return false;
      });
    }
    if (clientFilter.size > 0) {
      resources = resources.filter((r) => {
        const clientIds = clientIdsByResource.get(r.id);
        if (!clientIds) return false;
        for (const clientId of clientIds) {
          if (clientFilter.has(clientId)) {
            return true;
          }
        }
        return false;
      });
    }

    return { resourceMap, allocsByResource, resources };
  }, [
    visibleAssignments,
    eidFilterData,
    isDragging,
    contextAllocations,
    filters.chapters,
    filters.eids,
    filters.projectIds,
    filters.clientIds,
  ]); // eslint-disable-line react-hooks/exhaustive-deps

  // ─── Project groups (for project view) ────────────────────────────────────
  const projectGroups = useMemo(() => {
    const projectGroupMap = new Map<string, ProjectGroup>();
    const resourceRowMapByProject = new Map<
      string,
      Map<string, ProjectGroup["resourceRows"][number]>
    >();
    const chapterFilter = new Set(filters.chapters);
    const eidFilter = new Set(filters.eids);
    const clientFilter = new Set(filters.clientIds);
    const projectFilter = new Set(filters.projectIds);
    const allGroupEntries: TimelineProjectEntry[] = [...visibleAssignments, ...visibleDemands];
    for (const entry of allGroupEntries) {
      let group = projectGroupMap.get(entry.projectId);
      if (!group) {
        group = {
          id: entry.projectId,
          name: entry.project.name,
          shortCode: entry.project.shortCode,
          orderType: entry.project.orderType,
          clientId: entry.project.clientId ?? null,
          startDate: new Date(entry.project.startDate as unknown as string),
          endDate: new Date(entry.project.endDate as unknown as string),
          status: entry.project.status,
          color: (entry.project as { color?: string | null }).color ?? null,
          resourceRows: [],
        };
        projectGroupMap.set(entry.projectId, group);
        resourceRowMapByProject.set(entry.projectId, new Map());
      }
      const currentGroup = group;
      if (!currentGroup) continue;
      if (entry.kind === "assignment" && entry.resourceId) {
        const rowMap = resourceRowMapByProject.get(entry.projectId);
        const existingRow = rowMap?.get(entry.resourceId);
        if (existingRow) {
          existingRow.allocs.push(entry);
        } else {
          const res = resourceMap.get(entry.resourceId);
          if (res) {
            const row = { resource: res, allocs: [entry] };
            currentGroup.resourceRows.push(row);
            rowMap?.set(entry.resourceId, row);
          }
        }
      }
    }
    for (const group of projectGroupMap.values()) {
      group.resourceRows = group.resourceRows.filter(({ resource }) => {
        if (chapterFilter.size > 0) {
          if (!resource.chapter || !chapterFilter.has(resource.chapter)) {
            return false;
          }
        }
        if (eidFilter.size > 0 && !eidFilter.has(resource.eid)) {
          return false;
        }
        if (clientFilter.size > 0 && (!group.clientId || !clientFilter.has(group.clientId))) {
          return false;
        }
        return true;
      });
      group.resourceRows.sort((a, b) =>
        a.resource.displayName.localeCompare(b.resource.displayName),
      );
    }
    return [...projectGroupMap.values()]
      .sort((a, b) => a.startDate.getTime() - b.startDate.getTime())
      .filter((pg) => {
        if (projectFilter.size > 0 && !projectFilter.has(pg.id)) return false;
        if (
          clientFilter.size > 0 &&
          (!pg.clientId || !clientFilter.has(pg.clientId))
        )
          return false;
        if (
          chapterFilter.size > 0 &&
          pg.resourceRows.length === 0
        )
          return false;
        if (eidFilter.size > 0 && pg.resourceRows.length === 0)
          return false;
        return true;
      });
  }, [
    visibleAssignments,
    visibleDemands,
    resourceMap,
    filters.projectIds,
    filters.clientIds,
    filters.chapters,
    filters.eids,
  ]); // eslint-disable-line react-hooks/exhaustive-deps

  // ─── Derived counts ───────────────────────────────────────────────────────
  const isInitialLoading = (isRoleLoading || isLoading) && !entriesView;
  const totalAllocCount = entriesView?.allocations.length ?? 0;
  const activeFilterCount =
    filters.clientIds.length +
    filters.chapters.length +
    filters.eids.length +
    filters.projectIds.length +
    filters.countryCodes.length;

  const value = useMemo<TimelineContextValue>(
    () => ({
      assignments,
      demands,
      visibleAssignments,
      visibleDemands,
      vacationsByResource,
      resources,
      resourceMap,
      allocsByResource,
      projectGroups,
      openDemandsByProject,
      viewStart,
      viewEnd,
      viewDays,
      setViewStart,
      setViewDays,
      filters,
      setFilters,
      filterOpen,
      setFilterOpen,
      viewMode,
      setViewMode,
      today,
      displayMode,
      heatmapScheme,
      blinkOverbookedDays,
      isLoading,
      isInitialLoading,
      isEntriesError,
      totalAllocCount,
      activeFilterCount,
    }),
    [
      assignments,
      demands,
      visibleAssignments,
      visibleDemands,
      vacationsByResource,
      resources,
      resourceMap,
      allocsByResource,
      projectGroups,
      openDemandsByProject,
      viewStart,
      viewEnd,
      viewDays,
      filters,
      filterOpen,
      viewMode,
      today,
      displayMode,
      heatmapScheme,
      blinkOverbookedDays,
      isLoading,
      isInitialLoading,
      isEntriesError,
      totalAllocCount,
      activeFilterCount,
    ],
  );

  return <TimelineContext.Provider value={value}>{children}</TimelineContext.Provider>;
}
