/**
 * CalendarShell — Schedule-X calendar wired to TanStack Query + hydrateEvents + Zustand range.
 *
 * Data flow:
 *   Zustand calendarRange → TanStack Query ['events', start, end] → fetchEvents
 *   → hydrateEvents (ISO → Temporal) → eventsService.set() → Schedule-X render
 *
 * Critical constraints:
 *   - Plugins (eventsService) created once via useState stable initialiser
 *   - onRangeUpdate fires on navigation; initial fetch uses Zustand default range (A4/Q2)
 *   - DateRange.start/end are Temporal.ZonedDateTime → converted to 'YYYY-MM-DD' for Zustand
 *   - calendarId routing: 'shared' | String(ownerUserId) — produced by hydrateEvents, consumed
 *     by buildCalendarConfig; never the DB calendar-row id
 *   - Event popover: driven exclusively by Zustand openEventId via onEventClick → standalone
 *     EventDetailPopover; createEventModalPlugin and customComponents.eventModal are NOT used
 *   - Threat T-02d-01: all event fields are plain-text JSX children — no raw HTML injection
 *   - Navigation: Schedule-X's built-in header is the sole navigation bar (Today/‹›/view switcher
 *     + weekday-name row). The custom ViewToolbar and calendar-controls plugin have been removed.
 *
 * Layout (FIX 3):
 *   AppNav has been lifted to App.tsx and is no longer rendered here. CalendarShell fills
 *   its parent container (a flex:1 area in App.tsx). Auth splashes use position:fixed to
 *   overlay everything including AppNav.
 *
 * State branches:
 *   isLoading (initial) → SkeletonCalendar
 *   success + 0 occurrences → EmptyState
 *   isError (after retry:2) → error state with Retry button
 *   success + events → ScheduleXCalendar
 */

import { useState, useEffect, useMemo, useRef } from 'react';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { ScheduleXCalendar, useCalendarApp } from '@schedule-x/react';
import {
  createViewDay,
  createViewWeek,
  createViewMonthGrid,
  createViewMonthAgenda,
  type CalendarType,
} from '@schedule-x/calendar';
import { createEventsServicePlugin } from '@schedule-x/events-service';
import { Plus } from 'lucide-react';

import { fetchMe, fetchEvents } from '../api/client.js';
import { hydrateEvents } from '../lib/hydrateEvents.js';
import { maybeRedirectToLogin, clearLoginRedirect } from '../lib/loginRedirect.js';
import { buildCalendarConfig, SX_FIRST_DAY_OF_WEEK } from '../lib/calendarConfig.js';
import { useCalendarStore } from '../store/calendarStore.js';
import { AuthSplash } from './AuthSplash.js';
import { EventDetailPopover } from './EventDetailPopover.js';
import { EventForm } from './EventForm.js';
import { DeleteConfirmationDialog } from './DeleteConfirmationDialog.js';
import { SyncStateToast } from './SyncStateToast.js';
import { ColorLegend } from './ColorLegend.js';
import { SkeletonCalendar } from './SkeletonCalendar.js';
import { InstallPrompt } from './InstallPrompt.js';

// ── Helpers ────────────────────────────────────────────────────────────────

/**
 * IN-02: this does NOT compute the D-05 phone/desktop default — that lives in the
 * store's readPersistedView() (calendarStore.ts), which seeds selectedView with the
 * breakpoint-aware default. By the time this runs, `persistedView` already carries that
 * resolved default. The ONLY job here is the SSR guard: return a stable 'month-grid'
 * when `window` is undefined (no breakpoint to read), otherwise pass the already-resolved
 * view through unchanged. Do not expect breakpoint logic here.
 */
function resolveDefaultView(persistedView: string): string {
  if (typeof window === 'undefined') return 'month-grid';
  return persistedView;
}

function isPhone(): boolean {
  return typeof window !== 'undefined' && window.matchMedia('(max-width: 767px)').matches;
}

// ── Component ──────────────────────────────────────────────────────────────

export function CalendarShell() {
  // Use per-field selectors so CalendarShell does NOT subscribe to openEventId.
  // Without selectors, any popover open/close triggers a full re-render here,
  // which rebuilds the Schedule-X config and causes a visible calendar flash (Bug B).
  const calendarRange = useCalendarStore((s) => s.calendarRange);
  const setCalendarRange = useCalendarStore((s) => s.setCalendarRange);
  const setOpenEventId = useCalendarStore((s) => s.setOpenEventId);
  const selectedView = useCalendarStore((s) => s.selectedView);
  const setEventForm = useCalendarStore((s) => s.setEventForm);
  const eventFormOpen = useCalendarStore((s) => s.eventFormOpen);
  const sessionExpired = useCalendarStore((s) => s.sessionExpired);
  const { start, end } = calendarRange;
  const queryClient = useQueryClient();
  // Ref to ensure the session-expiry redirect timer fires only once per expiry
  const sessionExpiredRedirectFired = useRef(false);
  // When the one-shot redirect guard is already exhausted (flag set from a prior
  // navigation), maybeRedirectToLogin() returns false — arm the dead-end state so
  // AuthSplash shows the tap-to-retry recovery instead of spinning forever (D-11).
  const [loginRedirectExhausted, setLoginRedirectExhausted] = useState(false);

  // Fetch current user to build per-member color config.
  // Same query key (['me']) as App.tsx — TanStack Query deduplicates, no double fetch.
  const meQuery = useQuery({
    queryKey: ['me'],
    queryFn: fetchMe,
    retry: false,
    staleTime: 5 * 60 * 1000,
  });

  // Fetch windowed occurrences — key includes start/end so navigation refetches.
  //
  // enabled: meQuery.isSuccess is load-bearing for the OIDC login flow, not just
  // an optimization. When unauthenticated, every /api/* request hits the OIDC
  // guard, which 302-redirects to Authelia AND sets a fresh state cookie. If this
  // query ran concurrently with fetchMe (and retried), each /api/events redirect
  // would overwrite the OIDC state cookie mid-login — so the state returned to
  // /callback no longer matched the cookie, producing OAUTH_INVALID_RESPONSE
  // ("unexpected state parameter") and an Internal Server Error after Authelia.
  // Gating on a successful /api/me means only fetchMe (redirect:'manual',
  // retry:false) touches a guarded endpoint while unauthenticated, so the single
  // top-level /api/login navigation owns the state cookie uncontested.
  const eventsQuery = useQuery({
    queryKey: ['events', start, end],
    queryFn: () => fetchEvents(start, end),
    enabled: meQuery.isSuccess,
    retry: 2,
    staleTime: 5 * 60 * 1000,
  });

  // Create plugins once (stable across renders)
  const eventsService = useState(() => createEventsServicePlugin())[0];

  // Build members list from /api/me for ColorLegend (AppNav uses the same data from App.tsx)
  const members = useMemo(() => {
    if (!meQuery.data?.user) return [];
    return [
      {
        id: String(meQuery.data.user.id),
        name: meQuery.data.user.displayName ?? 'Member',
        color: meQuery.data.user.color,
      },
    ];
  }, [meQuery.data]);

  // Build calendars config whenever the authenticated user changes
  const calendarsConfig: Record<string, CalendarType> = useMemo(
    () => buildCalendarConfig(members).calendars,
    [members],
  );

  const defaultView = resolveDefaultView(selectedView);

  // Display events in the VIEWER's local timezone. Schedule-X defaults to 'UTC', which made
  // a 17:45-04:00 event render at 21:45 (9:45 PM). Events arrive as zoned ISO strings in their
  // own IANA zones (Toronto/Detroit/New_York/Edmonton); Schedule-X converts them to this one
  // display zone, so the family sees every event in their own wall-clock time.
  // IANATimezone (the config's timezone type) is declared but not exported by @schedule-x/calendar,
  // so derive it from useCalendarApp's config parameter rather than importing it.
  type SxTimeZone = NonNullable<Parameters<typeof useCalendarApp>[0]['timezone']>;
  const displayTimeZone: SxTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;

  // useCalendarApp — config is stable; plugins passed as second argument
  const calendar = useCalendarApp(
    {
      views: [createViewDay(), createViewWeek(), createViewMonthGrid(), createViewMonthAgenda()],
      defaultView,
      timezone: displayTimeZone,
      firstDayOfWeek: SX_FIRST_DAY_OF_WEEK,
      calendars: calendarsConfig,
      callbacks: {
        onRangeUpdate(range) {
          // range.start / range.end are Temporal.ZonedDateTime.
          // Set end to the day AFTER range.end (exclusive window end) so that:
          //   - Day view: start === day N, end === day N+1 → 1-day window (avoids 400 on 0-day span)
          //   - Week/Month: end includes the last visible day instead of dropping it
          setCalendarRange({
            start: range.start.toPlainDate().toString(),
            end: range.end.toPlainDate().add({ days: 1 }).toString(),
          });
        },
        onEventClick(event) {
          if (event.id != null) {
            setOpenEventId(String(event.id));
          }
        },
      },
    },
    [eventsService],
  );

  // Sync TanStack Query result into Schedule-X eventsService (Pitfall 4 guard)
  // hydrateEvents converts ISO strings → Temporal before eventsService.set()
  useEffect(() => {
    if (!eventsQuery.data) return;
    const sxEvents = hydrateEvents(eventsQuery.data.occurrences);
    eventsService.set(sxEvents);
  }, [eventsQuery.data, eventsService]);

  // Auth redirect — one-shot full-page nav to /api/login when /api/me fails.
  // If this is the first failure, maybeRedirectToLogin() sets a sessionStorage
  // flag and navigates the browser to /api/login (top-level nav, no CORS block).
  // The page will unmount as the browser navigates. If the flag is already set
  // (already bounced through login once and still failing), returns false — arm
  // loginRedirectExhausted so the dead-end "Tap to try again" branch renders
  // instead of spinning indefinitely (D-11 dead-end recovery).
  useEffect(() => {
    if (meQuery.isError) {
      const willRedirect = maybeRedirectToLogin();
      if (!willRedirect) {
        setLoginRedirectExhausted(true);
      }
    }
  }, [meQuery.isError]);

  // Clear the one-shot flag on a successful /api/me load so a later session
  // expiry can trigger another redirect instead of showing "Sign-in required".
  // Also reset the dead-end state in case the component is reused after auth recovery.
  useEffect(() => {
    if (meQuery.isSuccess) {
      clearLoginRedirect();
      setLoginRedirectExhausted(false);
    }
  }, [meQuery.isSuccess]);

  // Session-expiry redirect (D-11) — fires when a mid-use 401 is detected by the
  // global QueryCache/MutationCache handler and arms the Zustand sessionExpired flag.
  // Re-arms the one-shot guard (clearLoginRedirect) so maybeRedirectToLogin fires
  // afresh, then schedules the top-level navigation after ~1.5s (UI-SPEC §Surface 2
  // "≤2s before redirect, no dismiss button").
  useEffect(() => {
    if (!sessionExpired) return;
    if (sessionExpiredRedirectFired.current) return;
    sessionExpiredRedirectFired.current = true;

    clearLoginRedirect();
    const timer = setTimeout(() => {
      maybeRedirectToLogin();
    }, 1500);

    return () => {
      clearTimeout(timer);
    };
  }, [sessionExpired]);

  // ── Render helpers ────────────────────────────────────────────────────────

  // Determine which content to show in the calendar area.
  // Note: meQuery.isLoading is handled above by an early AuthSplash return — it
  // will always be false when we reach this line (meQuery.isSuccess is guaranteed).
  const isInitialLoading = eventsQuery.isLoading && !eventsQuery.data;
  const isEventsError = eventsQuery.isError;

  const phone = isPhone();

  // ── Auth splash (D-10) ────────────────────────────────────────────────────
  // Gate the calendar render on auth state so no calendar shell, skeleton, or
  // "Sign-in required" alert paints before Authelia (D-10 success criterion 5).
  //
  // Auth splashes use position:fixed (inset:0, z-index:999) to overlay the entire
  // viewport including the persistent AppNav in App.tsx (FIX 3).
  //
  // loading: meQuery is still pending — show the neutral "Signing you in" splash.
  //          No skeleton, no CalendarContent — nothing app-specific until authed.
  // isError: meQuery failed — the maybeRedirectToLogin() useEffect fires immediately
  //          after this render. Show the "redirecting" splash while the browser navigates.
  //          If the one-shot guard is exhausted (flag already set), maybeRedirectToLogin
  //          returns false and this splash stays — the dead-end "Tap to try again" is
  //          rendered by AuthSplash's dead-end state (wired in Task 3 via sessionExpired).
  if (meQuery.isLoading) {
    return <AuthSplash state="loading" overlay />;
  }

  if (meQuery.isError) {
    // loginRedirectExhausted: the one-shot guard was already set (prior navigation),
    // so maybeRedirectToLogin() returned false — show dead-end tap-to-retry (D-11).
    // Otherwise the useEffect above already fired maybeRedirectToLogin() and
    // the browser is navigating — show the redirecting splash while it does.
    if (loginRedirectExhausted) {
      return <AuthSplash state="dead-end" overlay />;
    }
    return <AuthSplash state="redirecting" overlay />;
  }

  // ── Session-expiry interstitial (D-11) ─────────────────────────────────────
  // When a mid-use query or mutation returns 401, the global QueryCache/MutationCache
  // handler sets sessionExpired=true. Show the "Session expired" interstitial while the
  // 1.5s timer fires (wired above in the sessionExpired useEffect).
  if (sessionExpired) {
    return (
      <AuthSplash
        state="redirecting"
        heading="Session expired"
        body="Signing you back in…"
        overlay
      />
    );
  }

  // ── Calendar content ───────────────────────────────────────────────────────

  // The content panel fills its parent container (a flex:1 area set by App.tsx).
  //
  // This is a plain JSX value, NOT a nested `function CalendarContent()` rendered
  // as `<CalendarContent />`. A component defined inside render has a new identity
  // every render, so React unmounts+remounts its entire subtree — including
  // <ScheduleXCalendar> — on ANY CalendarShell re-render (popup/form close, post-
  // write events refetch). That full remount is the "calendar flash" (Bug B). As
  // an element value it reconciles in place across re-renders: no remount, no flash.
  const calendarContent = (
    <div
      style={{
        flex: 1,
        minWidth: 0,
        display: 'flex',
        flexDirection: 'column',
        overflow: 'hidden',
      }}
    >
      {/* Main calendar area */}
      <div style={{ flex: 1, minHeight: 0, height: '100%', position: 'relative' }}>
        {isInitialLoading ? (
          // Loading state: shimmer skeleton
          <div style={{ padding: 'var(--space-4)', flex: 1 }}>
            <SkeletonCalendar variant={phone ? 'agenda' : 'month'} />
          </div>
        ) : isEventsError ? (
          // Error state: replace grid with heading + body + Retry button
          <div
            style={{
              display: 'flex',
              flexDirection: 'column',
              alignItems: 'center',
              justifyContent: 'center',
              padding: 'var(--space-12)',
              gap: 'var(--space-4)',
              fontFamily: 'var(--font-family-base)',
              textAlign: 'center',
              flex: 1,
            }}
          >
            <h2
              style={{
                margin: 0,
                fontSize: 'var(--text-heading-size)',
                fontWeight: 'var(--text-heading-weight)',
                lineHeight: 'var(--text-heading-line-height)',
                color: 'var(--color-text-primary)',
              }}
            >
              Couldn&apos;t load events
            </h2>
            <p
              style={{
                margin: 0,
                fontSize: 'var(--text-body-size)',
                color: 'var(--color-text-secondary)',
              }}
            >
              Check your connection and try again.
            </p>
            <button
              onClick={() => {
                void queryClient.refetchQueries({ queryKey: ['events'] });
              }}
              style={{
                background: 'var(--color-surface-dim)',
                border: '1px solid var(--color-border)',
                borderRadius: 'var(--space-1)',
                cursor: 'pointer',
                minHeight: '44px',
                padding: '0 var(--space-4)',
                fontSize: 'var(--text-label-size)',
                fontWeight: 600,
                color: 'var(--color-text-primary)',
                fontFamily: 'var(--font-family-base)',
              }}
            >
              Retry
            </button>
          </div>
        ) : (
          // Normal: Schedule-X calendar (primary focal point).
          // ALWAYS render the calendar even when the window has no events — its built-in
          // header carries the navigation, so swapping in an empty-state would strand the
          // user with no way to navigate away from an empty day/week. An empty grid is clear.
          <ScheduleXCalendar calendarApp={calendar} />
        )}
      </div>

      {/* Phone: show ColorLegend below toolbar, collapsed */}
      {phone && !meQuery.isLoading && members.length > 0 && (
        <div
          style={{
            padding: 'var(--space-2) var(--space-4)',
            borderTop: '1px solid var(--color-border)',
            background: 'var(--color-surface)',
          }}
        >
          <ColorLegend members={members} />
        </div>
      )}
    </div>
  );

  // ── Layout ─────────────────────────────────────────────────────────────────
  // CalendarShell fills its container (App.tsx provides the outer layout with AppNav).
  // Both phone and desktop use the same flex column structure here since AppNav is
  // now outside this component.

  return (
    <div
      style={{
        flex: 1,
        display: 'flex',
        flexDirection: 'column',
        background: 'var(--color-surface)',
        color: 'var(--color-text-primary)',
        fontFamily: 'var(--font-family-base)',
        overflow: 'hidden',
        height: '100%',
      }}
    >
      <InstallPrompt />

      {/* New Event toolbar button — desktop only (phone uses FAB below) */}
      {!phone && (
        <div
          style={{
            display: 'flex',
            justifyContent: 'flex-end',
            padding: 'var(--space-2) var(--space-4)',
            borderBottom: '1px solid var(--color-border-subtle)',
            background: 'var(--color-surface)',
            flexShrink: 0,
          }}
        >
          <button
            onClick={() => setEventForm(true, 'create')}
            style={{
              background: 'var(--color-text-primary)',
              color: '#ffffff',
              border: 'none',
              cursor: 'pointer',
              minHeight: '44px',
              padding: '0 var(--space-4)',
              borderRadius: 'var(--space-1)',
              fontSize: 'var(--text-label-size)',
              fontWeight: 600,
              display: 'flex',
              alignItems: 'center',
              gap: 'var(--space-1)',
              fontFamily: 'var(--font-family-base)',
            }}
          >
            <Plus size={16} aria-hidden="true" />
            {/* Plain text — XSS guard */}
            New Event
          </button>
        </div>
      )}

      {calendarContent}

      {/* Phone: New Event FAB — bottom-right floating action button (UI-SPEC §Interaction Contract) */}
      {phone && (
        <button
          aria-label="New Event"
          onClick={() => setEventForm(true, 'create')}
          style={{
            position: 'fixed',
            bottom: 'var(--space-6)',
            right: 'var(--space-6)',
            width: '56px',
            height: '56px',
            minWidth: '56px',
            minHeight: '56px',
            borderRadius: '50%',
            background: 'var(--color-text-primary)',
            color: '#ffffff',
            border: 'none',
            cursor: 'pointer',
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'center',
            boxShadow: '0 4px 16px rgba(0,0,0,0.18)',
            zIndex: 100,
            fontFamily: 'var(--font-family-base)',
          }}
        >
          <Plus size={24} aria-hidden="true" />
        </button>
      )}

      {/* EventDetailPopover — standalone mode driven by Zustand openEventId */}
      <EventDetailPopover />

      {/* EventForm modal — conditionally rendered while eventFormOpen */}
      {eventFormOpen && <EventForm />}

      {/* DeleteConfirmationDialog — always mounted; renders nothing when deleteDialogOpen is false */}
      <DeleteConfirmationDialog />

      {/* SyncStateToast — always mounted; renders nothing when lastSyncedUid is null */}
      <SyncStateToast />
    </div>
  );
}
