const { useEffect, useMemo, useRef, useState } = React;

const Bridge = () => window.ConstructProProgrammeBridge;
const Gantt = () => window.ConstructProGantt;

const STATUS_TONE = {
  planned: "info",
  in_progress: "success",
  completed: "success",
  delayed: "danger",
  at_risk: "warning"
};

const GANTT_LANE_META = [
  { key: "baseline", label: "Estimate", kind: "programme" },
  { key: "forecast", label: "Revised date", kind: "forecast" },
  { key: "progress", label: "Progress", kind: "progress" },
  { key: "actual", label: "Actual", kind: "actual" }
];

function resolveScheduleDates(source = {}) {
  const schedule = source.schedule || {};
  const estimateStart = schedule.baselineStart || schedule.plannedStart || source.startDate || source.programme?.startDate || "";
  const estimateEnd = schedule.baselineDue || schedule.plannedDue || source.endDate || source.programme?.endDate || "";
  const plannedStart = schedule.plannedStart || source.startDate || source.programme?.startDate || estimateStart;
  const plannedEnd = schedule.plannedDue || source.endDate || source.programme?.endDate || estimateEnd;
  const expectedStart = schedule.expectedStart || source.forecastStartDate || source.forecast?.startDate || plannedStart;
  const expectedEnd = schedule.expectedDue || source.forecastEndDate || source.forecast?.endDate || plannedEnd;
  const planShiftDays = schedule.planShiftDays || 0;
  const hasShift = Boolean(
    schedule.hasForecastShift
    || source.hasForecastShift
    || planShiftDays !== 0
    || (expectedStart && estimateStart && expectedStart !== estimateStart)
    || (expectedEnd && estimateEnd && expectedEnd !== estimateEnd)
    || (plannedStart && estimateStart && plannedStart !== estimateStart)
    || (plannedEnd && estimateEnd && plannedEnd !== estimateEnd)
  );
  const revisedStart = planShiftDays !== 0 ? plannedStart : expectedStart;
  const revisedEnd = planShiftDays !== 0 ? plannedEnd : expectedEnd;
  return {
    estimateStart,
    estimateEnd,
    expectedStart: revisedStart,
    expectedEnd: revisedEnd,
    hasShift,
    totalDelayDays: schedule.totalDelayDays || 0,
    cascadeDelayDays: schedule.cascadeDelayDays || 0,
    planShiftDays
  };
}

function formatScheduleRange(start, end) {
  const formatDate = window.ConstructProData?.formatScheduleDate;
  if (!formatDate || (!start && !end)) return "—";
  if (start && end) return `${formatDate(start)} → ${formatDate(end)}`;
  return formatDate(start || end);
}

function formatGanttDate(value) {
  const gantt = Gantt();
  if (!gantt?.formatShortDate || !value) return "—";
  return gantt.formatShortDate(value);
}

function ScheduleDatePair({ dates, compact = false, className = "" }) {
  const resolved = dates?.estimateStart ? dates : resolveScheduleDates(dates || {});
  const {
    estimateStart,
    estimateEnd,
    expectedStart,
    expectedEnd,
    hasShift,
    totalDelayDays
  } = resolved;

  return (
    <div className={`schedule-date-pair${compact ? " schedule-date-pair--compact" : ""}${className ? ` ${className}` : ""}`}>
      <div className="schedule-date-pair__row schedule-date-pair__row--estimate">
        <span className="schedule-date-pair__tag">Estimate</span>
        <span className="schedule-date-pair__range">{formatScheduleRange(estimateStart, estimateEnd)}</span>
      </div>
      {hasShift ? (
        <div className="schedule-date-pair__row schedule-date-pair__row--expected is-shifted">
          <span className="schedule-date-pair__tag">Revised</span>
          <span className="schedule-date-pair__range">{formatScheduleRange(expectedStart, expectedEnd)}</span>
          {totalDelayDays > 0 ? (
            <span className="schedule-date-pair__delay">+{totalDelayDays}d</span>
          ) : null}
        </div>
      ) : null}
    </div>
  );
}

function statusLabel(status) {
  return String(status || "planned").replace(/_/g, " ");
}

function programmeProgressLabel(act) {
  if (act?.progressLabel) return act.progressLabel;
  const bridge = Bridge();
  if (bridge?.formatProgressCountLabel) {
    return bridge.formatProgressCountLabel(act?.progressCount, act?.progress?.actual ?? 0);
  }
  return `${act?.progress?.actual ?? 0}%`;
}

function subtaskProgressHint(act) {
  if (act?.progressHint) return act.progressHint;
  const bridge = Bridge();
  return bridge?.formatSubtaskProgressHint?.(act?.progressCount) || "";
}

function WorkProgramme({ snapshot, onNav }) {
  const ff = window.ConstructProData.formatCurrency;
  const bridge = Bridge();
  const project = snapshot.project;
  const rollups = snapshot.rollups || {};
  const programme = snapshot.programme || window.ConstructProPlanning.getProgramme(snapshot.seed);
  const workItems = snapshot.workItems || [];
  const allWorkItems = snapshot.allWorkItems || workItems;
  const decoratedWorkItems = useMemo(
    () => bridge?.flattenDecoratedWorkItems?.(snapshot.projectTree || []) || [],
    [snapshot.projectTree, bridge]
  );

  const [phaseFilter, setPhaseFilter] = useState("all");
  const [blockFilter, setBlockFilter] = useState("all");
  const [search, setSearch] = useState("");
  const [zoom, setZoom] = useState(1);
  const [selectedId, setSelectedId] = useState(null);
  const [viewTab, setViewTab] = useState("gantt");
  const [mobileTimelineView, setMobileTimelineView] = useState(() => {
    const saved = sessionStorage.getItem("constructpro.programme.mobileView");
    return saved === "chart" ? "chart" : "list";
  });
  const isMobile = useIsPhone();

  const setMobileView = (mode) => {
    setMobileTimelineView(mode);
    sessionStorage.setItem("constructpro.programme.mobileView", mode);
  };

  const merged = useMemo(() => {
    if (!bridge) {
      return { activities: [], summary: {}, phases: [], events: [], asOnDate: "", projectBlocks: [] };
    }
    const asOnDate = programme.asOnDate || window.ConstructProData.projectAsOnDate();
    const scheduleProjections = window.ConstructProData.buildScheduleProjections(workItems, asOnDate);
    const activities = bridge.mergeActivitiesWithWorkItems(
      programme.activities || [],
      decoratedWorkItems.length ? decoratedWorkItems : workItems,
      project.id,
      { scheduleProjections, asOnDate, byBlock: true, blocks: project.blocks || [] }
    );
    const projectBlocks = bridge.resolveBlockList(
      project.blocks || [],
      decoratedWorkItems.length ? decoratedWorkItems : workItems,
      programme.activities || []
    );
    const summary = bridge.recalculateSummaryFromActivities(activities, allWorkItems);
    summary.totalBudget = rollups.budgetPlanned || project.budgetTotal || summary.totalBudget;
    if (rollups.progressPct != null) summary.overallProgress = rollups.progressPct;
    const phases = bridge.summarizePhases(programme.phases, activities, workItems, project.id);
    const events = bridge.buildDynamicEvents(programme.events || [], activities);
    return { activities, summary, phases, events, asOnDate, projectBlocks };
  }, [programme, workItems, decoratedWorkItems, allWorkItems, project.id, project.blocks, project.budgetTotal, project.budgetUsed, rollups]);

  const repairedBlocksRef = useRef(new Set());
  useEffect(() => {
    if (!project?.id) return;
    const blockNames = (project.blocks || [])
      .map((block) => (typeof block === "string" ? block : block?.name))
      .filter(Boolean);
    if (!blockNames.length) return;
    const eventBlocks = new Set(
      (programme.events || []).map((event) => event.blockName).filter(Boolean)
    );
    const needsRepair = blockNames.some((name) => !eventBlocks.has(name));
    if (!needsRepair || repairedBlocksRef.current.has(project.id)) return;
    repairedBlocksRef.current.add(project.id);
    window.ConstructProData.repairWorkHierarchy?.(project.id);
  }, [project?.id, project?.blocks, programme.events]);

  const selected = useMemo(
    () => merged.activities.find((a) => a.id === selectedId) || null,
    [merged.activities, selectedId]
  );

  const filteredEvents = useMemo(() => {
    const q = search.trim().toLowerCase();
    const filtered = merged.events.filter((evt) => {
      if (phaseFilter !== "all" && evt.phase !== phaseFilter) return false;
      if (blockFilter !== "all" && evt.blockName !== blockFilter) return false;
      if (q && !evt.name.toLowerCase().includes(q) && !(evt.blockName || "").toLowerCase().includes(q)) return false;
      return true;
    });
    const items = decoratedWorkItems.length ? decoratedWorkItems : workItems;
    if (!bridge) return filtered;
    return bridge.ensureGanttHierarchyVisibility(filtered, merged.events, items, project.id);
  }, [merged.events, phaseFilter, blockFilter, search, decoratedWorkItems, workItems, project.id, bridge]);

  const ganttTaskCount = useMemo(() => {
    if (!bridge) return 0;
    const items = decoratedWorkItems.length ? decoratedWorkItems : workItems;
    return bridge.buildGanttGroups(filteredEvents, items, project.id).length;
  }, [filteredEvents, decoratedWorkItems, workItems, project.id, bridge]);

  const openActivity = (activityId) => setSelectedId(activityId);
  const closeActivity = () => setSelectedId(null);

  const goToWorkItem = (workItemId) => {
    if (!workItemId) return;
    sessionStorage.setItem("constructpro.focusWorkItemId", workItemId);
    onNav?.("workItems");
  };

  const durationMonths = Math.max(1, Math.ceil((merged.summary.totalDuration || 0) / 30));

  if (!bridge) {
    return (
      <div className="fade-in">
        <PageHeader title="Work Programme" subtitle={project?.name || "Programme"} />
        <Card><p className="text-sm text-text-muted">Programme tools failed to load. Please refresh the page.</p></Card>
      </div>
    );
  }

  return (
    <div className="fade-in work-programme-page">
      <PageHeader
        title="Work Programme"
        subtitle={[project.location, merged.asOnDate && `As on ${merged.asOnDate}`, programme.completionDate && `Target ${programme.completionDate}`].filter(Boolean).join(" · ")}
        actions={
          <Button variant="secondary" onClick={() => onNav?.("finance")}>
            <Icons.finance size={18}/> Fund flow
          </Button>
        }
      />

      <div className="mb-6 grid grid-cols-2 gap-3 md:grid-cols-4 md:gap-4">
        <StatCard title="Total budget" value={ff(merged.summary.totalBudget)} trend={`${ff(rollups.budgetActual || project.budgetUsed || 0)} spent`} icon={<Icons.finance/>} tone="info"/>
        <StatCard title="Duration" value={`${durationMonths} mo`} trend={merged.summary.maxPlanShiftDays > 0 ? `+${merged.summary.maxPlanShiftDays}d revised · ${merged.summary.forecastShiftCount} updated` : merged.summary.maxExecutionSlip > 0 ? `+${merged.summary.maxExecutionSlip}d slip · ${merged.summary.totalDuration} days` : `${merged.summary.totalDuration} days estimated`} icon={<Icons.schedule/>} tone={merged.summary.maxPlanShiftDays > 0 || merged.summary.maxExecutionSlip > 0 ? "warning" : undefined}/>
        <StatCard title="Overall progress" value={`${merged.summary.overallProgress}%`} trend="From linked tasks" icon={<Icons.check/>} tone="success"/>
        <StatCard title="At risk / delayed" value={merged.summary.atRiskCount + merged.summary.delayedCount} trend={`${merged.summary.delayedCount} delayed · ${merged.summary.atRiskCount} at risk`} icon={<Icons.alert/>} tone="warning"/>
      </div>

      <div className="mb-4 flex flex-wrap gap-2" role="tablist" aria-label="Programme views">
        <button type="button" role="tab" aria-selected={viewTab === "gantt"} className={`rounded-lg px-4 py-2 text-sm font-semibold ${viewTab === "gantt" ? "bg-primary-container text-on-primary" : "bg-surface-container text-on-surface-variant"}`} onClick={() => setViewTab("gantt")}>
          Timeline
        </button>
        <button type="button" role="tab" aria-selected={viewTab === "phases"} className={`rounded-lg px-4 py-2 text-sm font-semibold ${viewTab === "phases" ? "bg-primary-container text-on-primary" : "bg-surface-container text-on-surface-variant"}`} onClick={() => setViewTab("phases")}>
          Phases & tasks
        </button>
      </div>

      {viewTab === "phases" ? (
        <PhasesPanel phases={merged.phases} activities={merged.activities} ff={ff} onOpenActivity={openActivity} onGoWorkItem={goToWorkItem}/>
      ) : (
        <>
          {isMobile && (
            <ProgrammeViewToggle
              value={mobileTimelineView}
              onChange={setMobileView}
              visibleCount={filteredEvents.length}
            />
          )}
          {isMobile && mobileTimelineView === "list" ? (
            <MobileProgrammeList
              events={filteredEvents}
              activities={merged.activities}
              workItems={decoratedWorkItems.length ? decoratedWorkItems : workItems}
              projectId={project.id}
              onOpen={openActivity}
            />
          ) : (
            <GanttPanel
              events={filteredEvents}
              activities={merged.activities}
              workItems={decoratedWorkItems.length ? decoratedWorkItems : workItems}
              projectId={project.id}
              phases={programme.phases}
              blocks={merged.projectBlocks}
              phaseFilter={phaseFilter}
              setPhaseFilter={setPhaseFilter}
              blockFilter={blockFilter}
              setBlockFilter={setBlockFilter}
              search={search}
              setSearch={setSearch}
              zoom={zoom}
              setZoom={setZoom}
              asOnDate={merged.asOnDate}
              maxCascadeDelay={merged.summary.maxPlanShiftDays || 0}
              maxExecutionSlip={merged.summary.maxExecutionSlip || 0}
              forecastShiftCount={merged.summary.forecastShiftCount || 0}
              onOpen={openActivity}
              visibleCount={ganttTaskCount}
              isMobile={isMobile}
            />
          )}
        </>
      )}

      {selected && (
        isMobile ? (
          <MobileSheet open title={selected.name} subtitle={statusLabel(selected.progress?.status)} onClose={closeActivity} footer={
            <Button variant="secondary" onClick={() => goToWorkItem(selected.workItemId)}>Open in Tasks</Button>
          }>
            <ActivityDetailBody selected={selected} ff={ff}/>
          </MobileSheet>
        ) : (
          <ActivityDetailPanel selected={selected} onClose={closeActivity} onGoWorkItem={goToWorkItem} ff={ff}/>
        )
      )}
    </div>
  );
}

function ProgrammeViewToggle({ value, onChange, visibleCount }) {
  return (
    <div className="programme-view-toggle mb-4" role="group" aria-label="Timeline display mode">
      <button
        type="button"
        className={`programme-view-toggle__btn ${value === "list" ? "is-active" : ""}`}
        aria-pressed={value === "list"}
        onClick={() => onChange("list")}
      >
        <Icons.viewList size={20}/>
        <span>List</span>
      </button>
      <button
        type="button"
        className={`programme-view-toggle__btn ${value === "chart" ? "is-active" : ""}`}
        aria-pressed={value === "chart"}
        onClick={() => onChange("chart")}
      >
        <Icons.viewTimeline size={20}/>
        <span>Chart</span>
      </button>
      <p className="programme-view-toggle__hint" aria-live="polite">
        {visibleCount} {visibleCount === 1 ? "activity" : "activities"}
      </p>
    </div>
  );
}

function PhasesPanel({ phases, activities, ff, onOpenActivity, onGoWorkItem }) {
  return (
    <div className="space-y-4">
      {phases.map((phase) => {
        const phaseActs = activities.filter((a) => a.phase === phase.id);
        return (
          <Card key={phase.id}>
            <div className="mb-3 flex flex-wrap items-center justify-between gap-2">
              <div>
                <h3 className="text-lg font-bold text-text-primary">{phase.name}</h3>
                <p className="text-xs text-text-muted">{phaseActs.length} activities · {phase.progressPct}% rollup</p>
              </div>
              {phase.workItemId && (
                <Button variant="ghost" className="text-xs" onClick={() => onGoWorkItem(phase.workItemId)}>Phase task</Button>
              )}
            </div>
            <div className="space-y-2">
              {phaseActs.map((act) => (
                <button
                  key={act.id}
                  type="button"
                  className="flex w-full items-center justify-between gap-3 rounded-lg border border-outline-variant bg-surface-container-lowest px-3 py-2.5 text-left hover:bg-surface-container"
                  onClick={() => onOpenActivity(act.id)}
                >
                  <div className="min-w-0">
                    <p className="truncate font-semibold text-text-primary">{act.name}</p>
                    {act.blockName ? <p className="text-[10px] font-semibold text-text-muted">{act.blockName}</p> : null}
                    <ScheduleDatePair dates={act} compact/>
                  </div>
                  <div className="flex shrink-0 flex-col items-end gap-1">
                    <Badge tone={STATUS_TONE[act.progress?.status] || "neutral"}>{programmeProgressLabel(act)}</Badge>
                    {act.progressCount?.total > 0 ? (
                      <span className="text-[10px] font-semibold text-text-muted">{subtaskProgressHint(act)}</span>
                    ) : null}
                    {act.linkedWorkItem && <span className="text-[10px] font-semibold uppercase text-primary">Linked</span>}
                  </div>
                </button>
              ))}
              {!phaseActs.length && <p className="text-sm text-text-muted">No programme activities in this phase.</p>}
            </div>
          </Card>
        );
      })}
    </div>
  );
}

function MobileProgrammeList({ events, activities, workItems, projectId, onOpen }) {
  const bridge = Bridge();
  const groups = useMemo(
    () => bridge.buildGanttGroups(events, workItems, projectId),
    [events, workItems, projectId, bridge]
  );

  return (
    <Card>
      <SectionTitle title="Activities" subtitle="Tap an activity for details and task sync"/>
      <div className="space-y-2">
        {groups.map((group) => {
          const act = activities.find((a) => a.id === group.event.id);
          const status = act?.progress?.status || "planned";
          return (
            <div key={group.event.id} className="rounded-lg border border-outline-variant overflow-hidden">
              <button type="button" className="w-full px-3 py-3 text-left bg-surface-container-lowest hover:bg-surface-container" onClick={() => onOpen(group.event.id)}>
                <div className="flex items-start justify-between gap-2">
                  <p className="font-semibold text-text-primary">{group.event.name}</p>
                  <Badge tone={STATUS_TONE[status] || "neutral"}>{statusLabel(status)}</Badge>
                </div>
                <p className="mt-1 text-xs text-text-muted">
                  {group.event.blockName ? <span className="font-semibold text-text-primary">{group.event.blockName}</span> : null}
                </p>
                <ScheduleDatePair dates={{ ...group.event, schedule: act?.schedule }} compact className="mt-1"/>
                {group.children.length > 0 ? (
                  <p className="mt-1 text-[10px] font-semibold text-primary">{group.children.length} subtask{group.children.length === 1 ? "" : "s"}</p>
                ) : null}
                <div className="mt-2">
                  <div className="mb-1 flex items-center justify-between text-xs text-text-muted">
                    <span>Progress</span>
                    <span className="font-semibold text-text-primary">{programmeProgressLabel(act)}</span>
                  </div>
                  {act?.progressCount?.total > 0 ? (
                    <p className="mb-1 text-[10px] font-semibold text-text-muted">{subtaskProgressHint(act)}</p>
                  ) : null}
                  <ProgressBar value={act?.progress?.actual ?? 0}/>
                </div>
              </button>
              {group.children.length > 0 ? (
                <div className="border-t border-outline-variant bg-surface-container/40">
                  {group.children.map((child) => {
                    const childAct = activities.find((a) => a.id === child.id);
                    const childBudget = ganttBudgetPlanned(child, childAct);
                    const ff = window.ConstructProData.formatCurrency;
                    return (
                      <button
                        key={child.id}
                        type="button"
                        className="flex w-full items-center justify-between gap-2 border-b border-outline-variant/60 px-3 py-2.5 pl-6 text-left last:border-b-0 hover:bg-surface-container"
                        onClick={() => onOpen(child.id)}
                      >
                        <div className="min-w-0">
                          <p className="truncate text-sm font-semibold text-text-primary">{child.name}</p>
                          <p className="text-[10px] text-text-muted">
                            Subtask
                            {childBudget > 0 ? ` · Est. ${ff(childBudget)}` : ""}
                          </p>
                        </div>
                        <span className="shrink-0 text-xs font-bold text-text-primary">{programmeProgressLabel(childAct)}</span>
                      </button>
                    );
                  })}
                </div>
              ) : null}
            </div>
          );
        })}
        {!groups.length && <p className="text-sm text-text-muted">No activities match your filters.</p>}
      </div>
    </Card>
  );
}

function GanttLaneLabel({ kind, children }) {
  return (
    <span className={`gantt-lane-label gantt-lane-label--${kind}`}>
      <span className={`gantt-lane-label__dot gantt-lane-label__dot--${kind}`} aria-hidden="true"/>
      <span className="gantt-lane-label__text">{children}</span>
    </span>
  );
}

const GANTT_SUBTASK_LANES = GANTT_LANE_META.filter((lane) => (
  lane.key === "baseline" || lane.key === "forecast" || lane.key === "progress"
));

function ganttBudgetPlanned(evt, act) {
  return Number(act?.budgetPlanned || evt?.programme?.totalEstimate || 0);
}

function GanttActivityBlock({
  evt,
  bar,
  act,
  layout,
  onOpen,
  bindRowScroll,
  variant = "parent",
  showBlockTag = true,
  childCount = 0,
  isGroupExpanded = false,
  onToggleGroup = null
}) {
  const isSubtask = variant === "subtask";
  const laneMeta = isSubtask ? GANTT_SUBTASK_LANES : GANTT_LANE_META;
  const blockName = evt.blockName || act?.blockName || "";
  const actualPct = act?.progress?.actual ?? 0;
  const status = act?.progress?.status || "planned";
  const statusClass = status === "delayed" ? "status-delayed" : status === "at_risk" ? "status-at-risk" : status === "completed" ? "status-completed" : "";
  const schedule = act?.schedule || {};
  const hasActualStart = Boolean(schedule.actualStart || evt.hasActualStart);
  const progressCount = act?.progressCount;
  const progressLabel = programmeProgressLabel(act);
  const subtaskHint = subtaskProgressHint(act);
  const budgetPlanned = ganttBudgetPlanned(evt, act);
  const ff = window.ConstructProData.formatCurrency;
  const actualLabel = progressCount?.total > 0
    ? progressLabel
    : hasActualStart ? `${actualPct}%` : "Not started";
  const scale = layout?.scale ?? 1;
  
  const scheduleDates = resolveScheduleDates({ ...evt, schedule, forecastStartDate: schedule.expectedStart, forecastEndDate: schedule.expectedDue, hasForecastShift: bar.hasForecastShift });
  const baselineTip = budgetPlanned > 0
    ? `Estimate: ${formatGanttDate(scheduleDates.estimateStart)} – ${formatGanttDate(scheduleDates.estimateEnd)} · ${ff(budgetPlanned)}`
    : `Estimate: ${formatGanttDate(scheduleDates.estimateStart)} – ${formatGanttDate(scheduleDates.estimateEnd)}`;
  const forecastTip = scheduleDates.hasShift
    ? `Revised date: ${formatGanttDate(scheduleDates.expectedStart)} – ${formatGanttDate(scheduleDates.expectedEnd)}`
    : baselineTip;
  const actualTip = schedule.actualStart
    ? `Actual: ${formatGanttDate(schedule.actualStart)} – ${schedule.actualEnd ? formatGanttDate(schedule.actualEnd) : "In progress"}`
    : "Actual: Not started yet";
  const progBarWidth = actualPct > 0 ? Math.max(6, bar.progress.width * scale) : 0;
  const progressLeft = bar.progress.x * scale;
  const forecastLeft = bar.forecast.x * scale;
  const forecastWidth = Math.max(6, bar.forecast.width * scale);
  const laneStyle = { width: layout.widthPx, "--gantt-month-w": `${layout.monthW}px` };
  const shiftDays = schedule.totalDelayDays || 0;

  const laneBars = {
    baseline: (
      <div
        className={`gantt-bar gantt-bar--programme${bar.hasForecastShift ? " gantt-bar--programme-muted" : ""}`}
        style={{ left: bar.programme.x * scale, width: Math.max(6, bar.programme.width * scale) }}
      />
    ),
    forecast: bar.hasForecastShift ? (
      <div
        className="gantt-bar gantt-bar--forecast"
        style={{ left: forecastLeft, width: forecastWidth }}
      />
    ) : null,
    progress: (
      <>
        <div className="gantt-bar gantt-bar--track" style={{ left: forecastLeft, width: forecastWidth }} aria-hidden="true"/>
        {progBarWidth > 0 ? (
          <div className={`gantt-bar gantt-bar--progress ${statusClass}`} style={{ left: progressLeft, width: progBarWidth }}>
            {progBarWidth > 48 && <span className="gantt-bar__label">{progressLabel}</span>}
          </div>
        ) : null}
        <span className="gantt-lane__hint gantt-lane__hint--progress">{progressLabel}</span>
      </>
    ),
    actual: hasActualStart ? (
      <div
        className={`gantt-bar gantt-bar--actual ${statusClass}`}
        style={{ left: bar.actual.x * scale, width: Math.max(6, bar.actual.width * scale) }}
      >
        {(progressCount?.total > 0 || actualPct > 0) && <span className="gantt-bar__label">{actualLabel}</span>}
      </div>
    ) : (
      <span className="gantt-lane__hint">{actualLabel}</span>
    )
  };

  const laneTips = {
    baseline: baselineTip,
    forecast: forecastTip,
    progress: `Work progress: ${progressLabel}`,
    actual: actualTip
  };

  return (
    <div className={`gantt-activity-block${bar.hasForecastShift ? " gantt-activity-block--shifted" : ""}${isSubtask ? " gantt-activity-block--subtask" : ""}`}>
      <button
        type="button"
        className="gantt-activity-banner"
        onClick={() => onOpen(evt.id)}
        aria-label={`${evt.name}. ${scheduleDates.hasShift ? forecastTip : baselineTip}. Status ${statusLabel(status)}.`}
      >
        {childCount > 0 ? (
          <span
            className="gantt-activity-expand"
            role="button"
            tabIndex={0}
            aria-expanded={isGroupExpanded}
            aria-label={isGroupExpanded ? "Collapse subtasks" : "Expand subtasks"}
            onClick={(e) => {
              e.stopPropagation();
              onToggleGroup?.();
            }}
            onKeyDown={(e) => {
              if (e.key === "Enter" || e.key === " ") {
                e.preventDefault();
                e.stopPropagation();
                onToggleGroup?.();
              }
            }}
          >
            <Icons.chevronRight size={16} className={isGroupExpanded ? "gantt-activity-expand__icon is-open" : "gantt-activity-expand__icon"}/>
          </span>
        ) : isSubtask ? (
          <span className="gantt-activity-tree" aria-hidden="true"/>
        ) : null}
        <span className="gantt-activity-banner__text">
          {showBlockTag && blockName ? <span className="gantt-activity-block-tag">{blockName}</span> : null}
          {isSubtask ? <span className="gantt-activity-subtask-tag">Subtask</span> : null}
          <span>{evt.name}</span>
        </span>
        <span className="gantt-activity-banner__meta">
          {childCount > 0 ? (
            <span className="gantt-activity-child-count">{childCount} subtask{childCount === 1 ? "" : "s"}</span>
          ) : null}
          {!isSubtask && shiftDays > 0 ? <span className="gantt-activity-shift">+{shiftDays}d</span> : null}
          {isSubtask && shiftDays > 0 ? <span className="gantt-activity-shift gantt-activity-shift--sub">+{shiftDays}d</span> : null}
          {!isSubtask ? (
            <span className={`gantt-activity-status gantt-activity-status--${status}`}>{statusLabel(status)}</span>
          ) : null}
          {budgetPlanned > 0 ? (
            <span className="gantt-activity-estimate" title="Estimated cost">{ff(budgetPlanned)}</span>
          ) : null}
          <span className="gantt-activity-pct" title={subtaskHint || undefined}>{progressLabel}</span>
          {!isSubtask && subtaskHint && !act?.progressCount?.total ? <span className="gantt-activity-subtasks">{subtaskHint}</span> : null}
          <Icons.chevronRight size={18} className="shrink-0 text-text-muted"/>
        </span>
      </button>
      <div className="gantt-activity-row">
        <div className={`gantt-lane-label-list${isSubtask ? " gantt-lane-label-list--compact" : ""}`} aria-hidden="true">
          {laneMeta.map((lane) => (
            <GanttLaneLabel key={lane.key} kind={lane.kind}>{lane.label}</GanttLaneLabel>
          ))}
        </div>
        <div className="gantt-board__timeline-scroll touch-scroll-x" ref={bindRowScroll} tabIndex={0}>
          <div className={`gantt-timeline-lanes${isSubtask ? " gantt-timeline-lanes--compact" : ""}`} style={laneStyle}>
            {laneMeta.map((lane) => (
              <div key={lane.key} className={`gantt-lane gantt-lane--${lane.kind}`} title={laneTips[lane.key]}>
                {laneBars[lane.key]}
              </div>
            ))}
          </div>
        </div>
      </div>
    </div>
  );
}

function GanttPanel({ events, activities, workItems, projectId, phases, blocks, phaseFilter, setPhaseFilter, blockFilter, setBlockFilter, search, setSearch, zoom, setZoom, asOnDate, maxCascadeDelay = 0, maxExecutionSlip = 0, forecastShiftCount = 0, onOpen, visibleCount, isMobile = false }) {
  const headerScrollRef = useRef(null);
  const syncLock = useRef(null);
  const [timelineWidth, setTimelineWidth] = useState(0);
  const bridge = Bridge();
  const [collapsedGroups, setCollapsedGroups] = useState({});

  const ganttGroups = useMemo(
    () => bridge.buildGanttGroups(events, workItems, projectId),
    [events, workItems, projectId, bridge]
  );

  const displayRows = useMemo(
    () => bridge.flattenGanttGroups(ganttGroups, collapsedGroups),
    [ganttGroups, collapsedGroups, bridge]
  );

  useEffect(() => {
    const el = headerScrollRef.current;
    if (!el) return undefined;
    const measure = () => setTimelineWidth(el.clientWidth || 0);
    measure();
    const observer = typeof ResizeObserver !== "undefined" ? new ResizeObserver(measure) : null;
    observer?.observe(el);
    window.addEventListener("resize", measure);
    return () => {
      observer?.disconnect();
      window.removeEventListener("resize", measure);
    };
  }, [displayRows.length, isMobile]);

  const barByEventId = useMemo(() => {
    if (!displayRows.length || !Gantt()) return new Map();
    const bounds = Gantt().getTimelineBounds(displayRows.map((row) => row.event));
    const bars = Gantt().layoutGanttBars(displayRows.map((row) => row.event), bounds);
    const monthCount = Math.max(1, bounds.months.length);
    const baseWidth = bounds.totalWidth;
    const minWidthPx = baseWidth * zoom;
    const widthPx = timelineWidth > 0 ? Math.max(minWidthPx, timelineWidth) : minWidthPx;
    const monthW = widthPx / monthCount;
    const scale = widthPx / baseWidth;
    const map = new Map();
    displayRows.forEach((row, index) => {
      map.set(row.event.id, { bar: bars[index], bounds, monthW, widthPx, scale });
    });
    return map;
  }, [displayRows, zoom, timelineWidth]);

  const layout = useMemo(() => {
    const first = barByEventId.values().next().value;
    if (!first) return null;
    return {
      bounds: first.bounds,
      monthW: first.monthW,
      widthPx: first.widthPx,
      scale: first.scale
    };
  }, [barByEventId]);

  const toggleGroup = (groupId) => {
    setCollapsedGroups((prev) => ({ ...prev, [groupId]: !prev[groupId] }));
  };

  const isGroupExpanded = (groupId) => !collapsedGroups[groupId];

  const bindRowScroll = (el) => {
    if (!el || !headerScrollRef.current) return;
    el.onscroll = () => {
      if (syncLock.current && syncLock.current !== el) return;
      syncLock.current = el;
      headerScrollRef.current.scrollLeft = el.scrollLeft;
      document.querySelectorAll(".gantt-board__timeline-scroll").forEach((row) => {
        if (row !== el) row.scrollLeft = el.scrollLeft;
      });
      requestAnimationFrame(() => { syncLock.current = null; });
    };
  };

  const scrollGantt = (dx) => {
    headerScrollRef.current?.scrollBy({ left: dx, behavior: "smooth" });
    document.querySelectorAll(".gantt-board__timeline-scroll").forEach((el) => el.scrollBy({ left: dx, behavior: "smooth" }));
  };

  const jumpToAsOn = () => {
    if (!layout || !asOnDate) return;
    const key = asOnDate.slice(0, 7);
    const idx = layout.bounds.months.findIndex((m) => m.key === key);
    if (idx < 0) return;
    const x = idx * layout.monthW;
    headerScrollRef.current?.scrollTo({ left: Math.max(0, x - 80), behavior: "smooth" });
    document.querySelectorAll(".gantt-board__timeline-scroll").forEach((el) => el.scrollTo({ left: Math.max(0, x - 80), behavior: "smooth" }));
  };

  if (!events.length) {
    return (
      <Card>
        <div className="py-12 text-center">
          <Icons.schedule size={40} className="mx-auto mb-3 text-text-muted"/>
          <p className="font-semibold text-text-primary">No programme activities</p>
          <p className="mt-1 text-sm text-text-muted">Add dated tasks in Work Management to build the programme timeline.</p>
        </div>
      </Card>
    );
  }

  if (!layout) {
    return (
      <Card>
        <div className="gantt-toolbar border-b border-outline-variant p-4">
          <div className="gantt-toolbar__filters flex flex-col gap-2">
            <input type="search" className="gantt-search w-full rounded-lg border border-outline-variant px-3 py-2.5 text-base md:text-sm" placeholder="Search activities or blocks…" value={search} onChange={(e) => setSearch(e.target.value)} aria-label="Search activities"/>
            <div className="gantt-toolbar__filter-row flex flex-col gap-2 md:flex-row">
              <select className="gantt-phase-select w-full rounded-lg border border-outline-variant px-3 py-2.5 text-base font-semibold md:text-sm md:flex-1" value={blockFilter} onChange={(e) => setBlockFilter(e.target.value)} aria-label="Filter by block">
                <option value="all">All blocks</option>
                {(blocks || []).map((block) => <option key={block.name} value={block.name}>{block.name}</option>)}
              </select>
              <select className="gantt-phase-select w-full rounded-lg border border-outline-variant px-3 py-2.5 text-base font-semibold md:text-sm md:flex-1" value={phaseFilter} onChange={(e) => setPhaseFilter(e.target.value)} aria-label="Filter by phase">
                <option value="all">All phases</option>
                {phases.map((p) => <option key={p.id} value={p.id}>{p.name}</option>)}
              </select>
            </div>
          </div>
        </div>
        <div className="py-12 text-center">
          <Icons.schedule size={40} className="mx-auto mb-3 text-text-muted"/>
          <p className="font-semibold text-text-primary">No timeline rows for this filter</p>
          <p className="mt-1 text-sm text-text-muted">Add dated tasks under this block in Work Management, or switch back to All blocks.</p>
        </div>
      </Card>
    );
  }

  const rangeLabel = layout.bounds.months.length
    ? `${layout.bounds.months[0].label} → ${layout.bounds.months[layout.bounds.months.length - 1].label}`
    : "Gantt timeline";

  return (
    <Card className={`gantt-panel overflow-hidden p-0 ${isMobile ? "gantt-panel--mobile" : ""}`}>
      {maxCascadeDelay > 0 ? (
        <div className="gantt-delay-banner border-b border-outline-variant bg-error-container/30 px-4 py-3 text-sm" role="status">
          <span className="font-semibold text-error">Plan revised</span>
          <span className="text-on-surface-variant">
            {" "}· Up to <strong>+{maxCascadeDelay} days</strong> from original estimate
            {forecastShiftCount > 0 ? ` · ${forecastShiftCount} activit${forecastShiftCount === 1 ? "y" : "ies"} updated` : ""}
            {asOnDate ? ` · As on ${asOnDate}` : ""}
          </span>
        </div>
      ) : null}
      {maxExecutionSlip > 0 && maxCascadeDelay === 0 ? (
        <div className="gantt-delay-banner border-b border-outline-variant bg-warning-container/30 px-4 py-3 text-sm" role="status">
          <span className="font-semibold text-warning">Schedule slip detected</span>
          <span className="text-on-surface-variant">
            {" "}· Up to <strong>+{maxExecutionSlip} days</strong> behind plan
            {asOnDate ? ` · As on ${asOnDate}` : ""}
          </span>
        </div>
      ) : null}
      <div className="gantt-toolbar border-b border-outline-variant p-4">
        <div className="gantt-toolbar__head mb-3 flex flex-wrap items-end justify-between gap-3">
          <div className="gantt-toolbar__meta min-w-0">
            <h3 className="gantt-toolbar__title text-base font-bold text-text-primary">Work programme</h3>
            <p className="gantt-toolbar__sub text-xs text-text-muted">{rangeLabel} · {visibleCount} visible</p>
          </div>
          <div className="gantt-legend" role="list" aria-label="Timeline legend">
            {GANTT_LANE_META.map((lane) => (
              <span key={lane.key} className="gantt-legend__item" role="listitem">
                <span className={`gantt-legend__swatch gantt-legend__swatch--${lane.kind}`}/>
                {lane.label}
              </span>
            ))}
          </div>
        </div>
        <div className="gantt-toolbar__filters flex flex-col gap-2">
          <input type="search" className="gantt-search w-full rounded-lg border border-outline-variant px-3 py-2.5 text-base md:text-sm" placeholder="Search activities or blocks…" value={search} onChange={(e) => setSearch(e.target.value)} aria-label="Search activities"/>
          <div className="gantt-toolbar__filter-row flex flex-col gap-2 md:flex-row">
            <select className="gantt-phase-select w-full rounded-lg border border-outline-variant px-3 py-2.5 text-base font-semibold md:text-sm md:flex-1" value={blockFilter} onChange={(e) => setBlockFilter(e.target.value)} aria-label="Filter by block">
              <option value="all">All blocks</option>
              {(blocks || []).map((block) => <option key={block.name} value={block.name}>{block.name}</option>)}
            </select>
            <select className="gantt-phase-select w-full rounded-lg border border-outline-variant px-3 py-2.5 text-base font-semibold md:text-sm md:flex-1" value={phaseFilter} onChange={(e) => setPhaseFilter(e.target.value)} aria-label="Filter by phase">
              <option value="all">All phases</option>
              {phases.map((p) => <option key={p.id} value={p.id}>{p.name}</option>)}
            </select>
          </div>
        </div>
        <div className="gantt-toolbar__controls mt-2 flex gap-1" role="toolbar" aria-label="Timeline navigation">
          <button type="button" className="gantt-btn" onClick={() => scrollGantt(-280)} aria-label="Scroll left"><Icons.chevronLeft size={22}/></button>
          <button type="button" className="gantt-btn" onClick={() => scrollGantt(280)} aria-label="Scroll right"><Icons.chevronRight size={22}/></button>
          <button type="button" className="gantt-btn gantt-btn--text" onClick={jumpToAsOn} aria-label="Jump to as-on date">
            <Icons.schedule size={20}/>
            {isMobile ? null : <span>Today</span>}
          </button>
          <div className="gantt-toolbar__zoom flex flex-1 gap-0 overflow-hidden rounded-lg border border-outline-variant">
            <button type="button" className="gantt-btn gantt-btn--zoom" onClick={() => setZoom((z) => Math.max(0.5, z - 0.25))} aria-label="Zoom out"><span aria-hidden="true">−</span></button>
            <button type="button" className="gantt-btn gantt-btn--zoom" onClick={() => setZoom((z) => Math.min(2, z + 0.25))} aria-label="Zoom in"><span aria-hidden="true">+</span></button>
          </div>
        </div>
      </div>

      {isMobile && (
        <p className="gantt-hint" id="gantt-search-hint">
          <span className="gantt-hint__text">
            <Icons.viewTimeline size={16}/>
            <span>Swipe timeline · tap activity name for details</span>
          </span>
          <span className="gantt-hint__count">{visibleCount} activities</span>
        </p>
      )}

      <div className={`gantt-board ${isMobile ? "gantt-board--mobile" : ""}`} role="region" aria-label="Work programme Gantt chart">
        <div className="gantt-board__header">
          <div className="gantt-board__label-corner">Activities</div>
          <div className="gantt-board__months touch-scroll-x" ref={headerScrollRef} onScroll={(e) => {
            const x = e.currentTarget.scrollLeft;
            if (syncLock.current) return;
            syncLock.current = "header";
            document.querySelectorAll(".gantt-board__timeline-scroll").forEach((el) => { el.scrollLeft = x; });
            requestAnimationFrame(() => { syncLock.current = null; });
          }}>
            <div className="gantt-board__months-inner" style={{ width: layout.widthPx, minWidth: timelineWidth > layout.widthPx ? undefined : "100%" }}>
              {layout.bounds.months.map((m) => (
                <div key={m.key} className="gantt-month-cell" style={{ width: layout.monthW, flexBasis: layout.monthW }}>{m.label}</div>
              ))}
            </div>
          </div>
        </div>
        <div className="gantt-board__body">
          {ganttGroups.map((group) => {
            const parentLayout = barByEventId.get(group.event.id);
            if (!parentLayout) return null;
            const expanded = isGroupExpanded(group.event.id);
            return (
              <div
                key={group.event.id}
                className={`gantt-activity-group${group.children.length ? " gantt-activity-group--has-children" : ""}${expanded ? " is-expanded" : " is-collapsed"}`}
              >
                <GanttActivityBlock
                  evt={group.event}
                  bar={parentLayout.bar}
                  act={activities.find((a) => a.id === group.event.id)}
                  layout={layout}
                  onOpen={onOpen}
                  bindRowScroll={bindRowScroll}
                  childCount={group.children.length}
                  isGroupExpanded={expanded}
                  onToggleGroup={() => toggleGroup(group.event.id)}
                />
                {expanded && group.children.map((child) => {
                  const childLayout = barByEventId.get(child.id);
                  if (!childLayout) return null;
                  return (
                    <GanttActivityBlock
                      key={child.id}
                      evt={child}
                      bar={childLayout.bar}
                      act={activities.find((a) => a.id === child.id)}
                      layout={layout}
                      onOpen={onOpen}
                      bindRowScroll={bindRowScroll}
                      variant="subtask"
                      showBlockTag={false}
                    />
                  );
                })}
              </div>
            );
          })}
        </div>
      </div>
    </Card>
  );
}

/* ── Activity detail – clear, visual, read-only ── */
function ActivityDetailBody({ selected, ff }) {
  const actualPct  = selected.progress?.actual  ?? 0;
  const plannedPct = selected.progress?.planned ?? 0;
  const variance   = actualPct - plannedPct;
  const status     = selected.progress?.status  || "planned";
  const linked     = selected.linkedWorkItem;
  const schedule   = selected.schedule || {};
  const formatDate = window.ConstructProData.formatScheduleDate;
  const subtaskHint = subtaskProgressHint(selected);

  // Status banner config — Material icons, no emojis
  const statusConfig = {
    completed:   { icon: <Icons.taskAlt size={20}/>,      color: "act-status--done",     msg: "Work completed"           },
    in_progress: { icon: <Icons.construction size={20}/>, color: "act-status--progress", msg: "Work in progress"         },
    delayed:     { icon: <Icons.errorOutline size={20}/>, color: "act-status--delayed",  msg: "Behind schedule — delayed" },
    at_risk:     { icon: <Icons.warningAmber size={20}/>, color: "act-status--risk",     msg: "Slightly behind schedule"  },
    planned:     { icon: <Icons.hourglass size={20}/>,    color: "act-status--planned",  msg: "Not started yet"          },
  };
  const sc = statusConfig[status] || statusConfig.planned;

  // Days left / overdue — use live forecast end when shifted
  const asOnRef  = selected.asOnDate || window.ConstructProData.projectAsOnDate?.() || new Date().toISOString().slice(0, 10);
  const today    = new Date(`${asOnRef}T12:00:00`);
  const endDate  = new Date(`${schedule.expectedDue || selected.forecastEndDate || selected.endDate}T12:00:00`);
  const daysLeft = Math.round((endDate - today) / 86400000);
  const dateNote = status === "completed"
    ? "Finished"
    : daysLeft > 0   ? `${daysLeft} days left`
    : daysLeft === 0 ? "Due today"
    : `${Math.abs(daysLeft)} days overdue`;
  const dateNoteTone = status === "completed" ? "act-days--done"
    : daysLeft >= 0  ? "act-days--ok"
    : "act-days--late";

  return (
    <div className="act-detail">

      {/* ① Status banner */}
      <div className={`act-status ${sc.color}`}>
        <span className="act-status__icon">{sc.icon}</span>
        <span className="act-status__msg">{sc.msg}</span>
      </div>

      {/* ② Progress snapshot — hero % + side metrics */}
      <div className="act-progress-split">
        <div className={`act-progress-hero act-progress-hero--${status}`}>
          <span className="act-progress-hero__num">{actualPct}%</span>
          <span className="act-progress-hero__lbl">Complete</span>
        </div>
        <div className="act-progress-side">
          <div className="act-progress-side__item">
            <span className="act-progress-side__lbl">Should be</span>
            <span className="act-progress-side__val">{plannedPct}%</span>
          </div>
          <div className="act-progress-side__item">
            <span className="act-progress-side__lbl">{variance < 0 ? "Behind" : variance > 0 ? "Ahead" : "On track"}</span>
            <span className={`act-progress-side__val ${variance < 0 ? "is-behind" : variance > 0 ? "is-ahead" : ""}`}>
              {variance === 0 ? "—" : variance > 0 ? `+${variance}%` : `${variance}%`}
            </span>
          </div>
        </div>
      </div>

      {/* ③ Visual progress bar */}
      <div className="act-bar-wrap">
        <div className="act-bar-track">
          <div
            className={`act-bar-fill ${status === "delayed" ? "act-bar-fill--delayed" : status === "at_risk" ? "act-bar-fill--risk" : status === "completed" ? "act-bar-fill--done" : "act-bar-fill--progress"}`}
            style={{ width: `${actualPct}%` }}
          />
          {plannedPct > 0 && plannedPct < 100 && (
            <div className="act-bar-tick" style={{ left: `${plannedPct}%` }} title={`Target: ${plannedPct}%`}>
              <span className="act-bar-tick__lbl">{plannedPct}%</span>
            </div>
          )}
        </div>
        <div className="act-bar-legend">
          <span>0%</span>
          <span className={`act-days ${dateNoteTone}`}>
            {selected.progressCount?.total > 0
              ? `${subtaskHint} · ${dateNote}`
              : dateNote}
          </span>
          <span>100%</span>
        </div>
      </div>

      {selected.subtasks?.length > 0 ? (
        <div className="act-subtasks">
          <div className="act-subtasks__header">
            <span className="act-subtasks__title">Subtasks</span>
            <span className="act-subtasks__meta">{subtaskHint}</span>
          </div>
          <ul className="act-subtasks__list">
            {selected.subtasks.map((sub) => (
              <li key={sub.id} className={`act-subtasks__item${sub.done ? " is-done" : sub.progressPct > 0 ? " is-active" : ""}`}>
                <span className="act-subtasks__status" aria-hidden="true">
                  {sub.done ? <Icons.check size={12}/> : sub.progressPct > 0 ? <Icons.construction size={12}/> : null}
                </span>
                <div className="act-subtasks__body">
                  <span className="act-subtasks__name">{sub.title}</span>
                  {sub.progressPct > 0 && !sub.done ? (
                    <span className="act-subtasks__pct">{sub.progressPct}%</span>
                  ) : null}
                </div>
                <Badge tone={sub.done ? "success" : sub.status === "blocked" ? "danger" : sub.progressPct > 0 ? "info" : "neutral"}>
                  {sub.done ? "Done" : String(sub.status || "todo").replace(/_/g, " ")}
                </Badge>
              </li>
            ))}
          </ul>
        </div>
      ) : null}

      {/* ④ Timeline & cost cards */}
      <div className="act-info-row act-info-row--dates">
        <div className="act-info-card act-info-card--estimate">
          <span className="act-info-card__icon-wrap act-info-card__icon-wrap--estimate">
            <Icons.calendarMonth size={18}/>
          </span>
          <div className="min-w-0 flex-1">
            <p className="act-info-card__lbl">Estimate</p>
            <p className="act-info-card__val">{formatDate(schedule.plannedStart || selected.startDate)} → {formatDate(schedule.plannedDue || selected.endDate)}</p>
            <p className="act-info-card__sub">Original plan · {selected.duration} days</p>
          </div>
        </div>

        {schedule.hasForecastShift ? (
        <div className="act-info-card act-info-card--expected is-shifted">
          <span className="act-info-card__icon-wrap act-info-card__icon-wrap--expected">
            <Icons.schedule size={18}/>
          </span>
          <div className="min-w-0 flex-1">
            <p className="act-info-card__lbl">Revised date</p>
            <p className="act-info-card__val act-info-card__val--forecast">
              {formatDate(schedule.expectedStart)} → {formatDate(schedule.expectedDue)}
            </p>
            <p className="act-info-card__sub act-info-card__sub--warn">
              +{schedule.totalDelayDays || 0}d slip
              {schedule.cascadeDelayDays > 0 ? ` (${schedule.cascadeDelayDays}d from upstream tasks)` : ""}
            </p>
          </div>
        </div>
        ) : null}

        <div className="act-info-card act-info-card--actual-dates">
          <span className="act-info-card__icon-wrap act-info-card__icon-wrap--actual">
            <Icons.construction size={18}/>
          </span>
          <div className="min-w-0 flex-1">
            <p className="act-info-card__lbl">Actual</p>
            <p className="act-info-card__val act-info-card__val--compact">
              {schedule.actualStart ? formatDate(schedule.actualStart) : "Not started"}
              {" → "}
              {schedule.actualEnd ? formatDate(schedule.actualEnd) : schedule.actualStart ? "In progress" : "—"}
            </p>
            <p className="act-info-card__sub">Work done on site</p>
          </div>
        </div>
      </div>

      {(selected.budgetPlanned > 0 || selected.programmeRow?.totalEstimate > 0) && (() => {
          const budgetP = selected.budgetPlanned || selected.programmeRow?.totalEstimate || 0;
          const budgetA = selected.budgetActual || 0;
          const spendPct = budgetP > 0 ? Math.min(100, Math.round((budgetA / budgetP) * 100)) : 0;
          const spendTone = spendPct > 90 ? "act-spend--over" : spendPct > 60 ? "act-spend--warn" : "act-spend--ok";
          return (
            <div className="act-info-row">
            <div className="act-cost-card">
              <div className="act-cost-card__row">
                <span className="act-cost-card__icon-wrap act-cost-card__icon-wrap--planned">
                  <Icons.payments size={15}/>
                </span>
                <span className="act-cost-card__lbl">Planned budget</span>
                <span className="act-cost-card__val">{ff(budgetP)}</span>
              </div>
              <div className="act-cost-card__row">
                <span className="act-cost-card__icon-wrap act-cost-card__icon-wrap--actual">
                  <Icons.finance size={15}/>
                </span>
                <span className="act-cost-card__lbl">Actual spent</span>
                <span className={`act-cost-card__val act-cost-card__val--actual ${budgetA === 0 ? "act-cost-card__val--zero" : ""}`}>
                  {budgetA > 0 ? ff(budgetA) : "Not started"}
                </span>
              </div>
              <div className="act-cost-card__bar-track">
                <div className={`act-cost-card__bar-fill ${spendTone}`} style={{ width: `${spendPct}%` }}/>
              </div>
              <p className="act-cost-card__sub">{spendPct}% of budget used</p>
            </div>
            </div>
          );
        })()}

      {/* ⑤ Linked task chip */}
      {linked ? (
        <div className="act-linked">
          <span className="act-linked__icon-wrap">
            <Icons.link size={18}/>
          </span>
          <div className="min-w-0">
            <p className="act-linked__title">{linked.title}</p>
            <p className="act-linked__sub">
              {selected.blockName ? `${selected.blockName} · ` : ""}Linked task · status: {linked.status}
            </p>
          </div>
        </div>
      ) : (
        <div className="act-linked act-linked--unlinked">
          <span className="act-linked__icon-wrap act-linked__icon-wrap--warn">
            <Icons.warningAmber size={18}/>
          </span>
          <p className="act-linked__title">No task linked yet — showing programme estimate</p>
        </div>
      )}
    </div>
  );
}

function ActivityDetailPanel({ selected, onClose, onGoWorkItem, ff }) {
  return (
    <>
      <div className="programme-panel-overlay" onClick={onClose} aria-hidden="true"/>
      <aside className="programme-detail-panel" role="dialog" aria-modal="true" aria-labelledby="programme-panel-title">
        <div className="flex items-center justify-between border-b border-outline-variant p-4">
          <h2 id="programme-panel-title" className="text-lg font-bold text-text-primary">Activity details</h2>
          <button type="button" className="m3-icon-button" onClick={onClose} aria-label="Close"><Icons.close size={22}/></button>
        </div>
        <div className="overflow-y-auto p-4">
          <p className="act-panel-eyebrow">{selected.blockName || "Activity"}</p>
          <h3 className="act-panel-title">{selected.name}</h3>
          <ActivityDetailBody selected={selected} ff={ff}/>
        </div>
        <div className="flex gap-2 border-t border-outline-variant p-4">
          <Button className="flex-1" onClick={() => onGoWorkItem(selected.workItemId)}>Open in Tasks</Button>
        </div>
      </aside>
    </>
  );
}

function Schedule(props) {
  return <WorkProgramme {...props}/>;
}

Object.assign(window, { WorkProgramme, Schedule });
