'use client';

import { useEffect, useState } from 'react';
import { useQuery } from '@tanstack/react-query';

import { useRealtimeInvalidation } from '@/hooks/use-realtime-invalidation';
import { useDashboardWidgets } from '@/hooks/use-dashboard-widgets';
import { apiFetch } from '@/lib/api/client';
import { PageHeader } from '@/components/shared/page-header';
import { CustomizeWidgetsMenu } from './customize-widgets-menu';
import { DateRangePicker } from './date-range-picker';
import { TimezoneDriftBanner } from './timezone-drift-banner';
import { WidgetErrorBoundary } from './widget-error-boundary';
import type { DashboardWidget } from './widget-registry';
import { isCustomRange, type DateRange } from '@/lib/analytics/range';

const PRESET_LABELS: Record<'today' | '7d' | '30d' | '90d', string> = {
  today: 'Today',
  '7d': 'Last 7 days',
  '30d': 'Last 30 days',
  '90d': 'Last 90 days',
};

function rangeLabel(range: DateRange): string {
  if (isCustomRange(range)) {
    const fmt: Intl.DateTimeFormatOptions = {
      month: 'short',
      day: 'numeric',
      year: 'numeric',
      timeZone: 'UTC',
    };
    const from = new Date(`${range.from}T00:00:00.000Z`).toLocaleDateString('en-US', fmt);
    const to = new Date(`${range.to}T00:00:00.000Z`).toLocaleDateString('en-US', fmt);
    return `${from} – ${to}`;
  }
  return PRESET_LABELS[range];
}

interface MeData {
  data?: {
    profile?: {
      firstName?: string | null;
      displayName?: string | null;
    };
  };
}

function timeOfDayGreeting(): string {
  // new Date().getHours() uses the browser's local timezone, which the OS
  // updates automatically on most modern devices when the user travels (laptop
  // "set timezone automatically" on macOS, phones via cell-tower / GPS). So
  // this reflects the user's physical location as long as their machine clock
  // does. We still defer the compute to a useEffect on mount so the rendered
  // HTML can never disagree with the server's clock during hydration.
  const hour = new Date().getHours();
  if (hour < 5) return 'Up late';
  if (hour < 12) return 'Good morning';
  if (hour < 18) return 'Good afternoon';
  return 'Good evening';
}

export function DashboardShell() {
  const [range, setRange] = useState<DateRange>('30d');

  const { visibleWidgets } = useDashboardWidgets();

  // Bucket once so the JSX stays readable. Registry order is preserved
  // inside each bucket, so reordering the registry reorders the render.
  const charts = visibleWidgets.filter((w) => w.group === 'chart');
  const rails = visibleWidgets.filter((w) => w.group === 'rail');
  const feed = visibleWidgets.filter((w) => w.group === 'feed');

  // Reuses the existing ['me'] cache (5-minute staleTime) populated by
  // useTablePreferences elsewhere — usually a cache hit, so no extra
  // request. Falls back to a generic greeting if the profile isn't
  // available yet so we never block the dashboard render.
  const me = useQuery<MeData>({
    queryKey: ['me'],
    queryFn: ({ signal }) => apiFetch<MeData>('/api/v1/me', { signal }),
    staleTime: 5 * 60_000,
  });
  const firstName = me.data?.data?.profile?.firstName?.trim();

  // Greeting word is computed in a useEffect so the rendered HTML can't lock
  // to the server's clock during hydration. Until the effect fires, the
  // header reads "Welcome" — a neutral phrase that's correct at every hour
  // and never produces a hydration warning. `clientGreeting` flips to the
  // local-time-aware phrasing once the component has mounted.
  const [clientGreeting, setClientGreeting] = useState<string | null>(null);
  useEffect(() => {
    setClientGreeting(timeOfDayGreeting());
    // Re-evaluate hourly so a rep who leaves the dashboard open through a
    // boundary (5am, noon, 6pm) doesn't keep stale text on screen.
    const interval = window.setInterval(
      () => setClientGreeting(timeOfDayGreeting()),
      60 * 60_000,
    );
    return () => window.clearInterval(interval);
  }, []);

  const greeting = firstName
    ? `${clientGreeting ?? 'Welcome'}, ${firstName}`
    : (clientGreeting ?? 'Welcome back');

  // Use a partial query-key prefix (no range segment) for invalidations.
  // Reading: "any cached analytics result, regardless of range, please
  // refetch on this event." This avoids any chance that a custom-range
  // object literal hashes differently than the one stored in the cache,
  // and keeps the invalidation surface broad enough to refresh whichever
  // range the user is currently looking at.
  useRealtimeInvalidation({
    'interest:stageChanged': [
      ['analytics', 'pipeline_funnel'],
      ['analytics', 'lead_source_attribution'],
      ['dashboard', 'kpis'],
    ],
    'client:created': [['dashboard', 'kpis']],
    'berth:statusChanged': [
      ['analytics', 'occupancy_timeline'],
      ['dashboard', 'kpis'],
    ],
  });

  return (
    <div className="space-y-6">
      {/* Mobile-only greeting strip. The shared PageHeader is hidden
          below `sm` (its title is normally duplicated by the topbar),
          so we render the welcome message inline here for mobile —
          keeps the personalized touch from desktop without polluting
          the topbar (which stays "Dashboard" for wayfinding). */}
      <div className="sm:hidden">
        <p className="text-xs font-semibold uppercase tracking-wide text-brand">Dashboard</p>
        <h1 className="mt-1 text-xl font-bold tracking-tight text-foreground">{greeting}</h1>
      </div>

      <TimezoneDriftBanner />

      <PageHeader
        title={greeting}
        eyebrow="Dashboard"
        // The date-range subtitle only means something when at least
        // one widget is on the page to consume the range; if everything
        // is hidden it just reads as an orphaned line.
        kpiLine={visibleWidgets.length > 0 ? <span>{rangeLabel(range)}</span> : undefined}
        variant="gradient"
        actions={
          <div className="flex items-center gap-2">
            <DateRangePicker value={range} onChange={setRange} />
            <CustomizeWidgetsMenu />
          </div>
        }
      />

      {/* Charts + rails sit side-by-side at xl+. Each side is an auto-fit
          grid, so hiding a card causes the remaining ones to widen.
          `items-start` is critical: without it, the right-column aside is
          stretched to match the chart column's row height, which forces
          MyRemindersRail (or any other child with `h-full`) to push later
          children out of the aside's box. */}
      {/* Charts + rails. Layout adapts to which regions have content so
          we never leave a 320px stripe of dead space when only one side
          is populated:
            both → main 1fr column + 320px rail (the original layout)
            charts only → single full-width auto-fit chart grid
            rails only → rails widen into an auto-fit grid (no fixed 320)
            neither → nothing renders
          The chart grid uses `minmax(360px, 1fr)` so a lone chart fills
          the row; the rails-only grid uses a slightly tighter `280px`
          minimum so KPI tiles + rails fit 3-4 across on a wide viewport
          instead of stretching to 600px+ each. */}
      {charts.length > 0 && rails.length > 0 ? (
        <div className="grid gap-4 grid-cols-1 items-start xl:grid-cols-[minmax(0,1fr)_320px]">
          <div className="grid gap-4 grid-cols-1 lg:grid-cols-[repeat(auto-fit,minmax(360px,1fr))]">
            {charts.map((w) => (
              <WidgetCell key={w.id} widget={w} range={range} />
            ))}
          </div>
          <aside className="min-w-0 space-y-4">
            {rails.map((w) => (
              <WidgetCell key={w.id} widget={w} range={range} />
            ))}
          </aside>
        </div>
      ) : charts.length > 0 ? (
        <div className="grid gap-4 grid-cols-1 lg:grid-cols-[repeat(auto-fit,minmax(360px,1fr))]">
          {charts.map((w) => (
            <WidgetCell key={w.id} widget={w} range={range} />
          ))}
        </div>
      ) : rails.length > 0 ? (
        <div className="grid gap-4 grid-cols-1 sm:grid-cols-[repeat(auto-fit,minmax(280px,1fr))]">
          {rails.map((w) => (
            <WidgetCell key={w.id} widget={w} range={range} />
          ))}
        </div>
      ) : null}

      {feed.map((w) => (
        <WidgetCell key={w.id} widget={w} range={range} />
      ))}

      {visibleWidgets.length === 0 ? <EmptyDashboardHint /> : null}
    </div>
  );
}

/**
 * Placeholder shown when the rep has hidden every widget. Without this,
 * the dashboard collapses to just the gradient header strip and looks
 * like a broken page — this hints at the "Customize" button to bring
 * widgets back.
 */
function EmptyDashboardHint() {
  return (
    <div className="flex flex-col items-center justify-center gap-2 rounded-lg border border-dashed border-border bg-card/40 px-6 py-16 text-center">
      <p className="text-sm font-medium text-foreground">No widgets on your dashboard yet</p>
      <p className="max-w-sm text-sm text-muted-foreground">
        Click <span className="font-medium text-foreground">Customize</span> above to pick which
        analytics cards appear here.
      </p>
    </div>
  );
}

function WidgetCell({ widget, range }: { widget: DashboardWidget; range: DateRange }) {
  return <WidgetErrorBoundary>{widget.render(range)}</WidgetErrorBoundary>;
}
