/**
 * ListDetail — Single list view (/lists/:listId).
 *
 * Delivers LIST-02 + LIST-03 + LIST-04:
 *   - Fetches items via useQuery(['list', listId]) with 30s polling fallback (D-12)
 *   - Splits items into active (!checked, sorted rank ASC) and completed (checked) (D-05)
 *   - Optimistic mutations for add / check / delete (D-07/D-09)
 *   - Delete-wins: no rollback on item delete (D-09)
 *   - Per-field LWW PATCH: check/uncheck sends { checked } only (D-08)
 *   - Drag-to-reorder (dnd-kit): DndContext + SortableContext over active items (LIST-03, D-13)
 *     - onDragEnd: computes generateKeyBetween, PATCHes { position } optimistically, rolls back on error
 *     - TouchSensor with 200ms delay + 5px tolerance — no accidental scroll drags
 *     - KeyboardSensor for accessibility keyboard-reorder fallback
 *   - Remote reorders animate via CSS.Transform.toString in ItemRow (D-14)
 *   - Concurrent reorder converges via server last-write-wins (D-15)
 *
 * Live sync (LIST-04, D-10/D-11/D-12):
 *   - useListSSE wires /api/sse/lists with bounded backoff reconnect (D-11)
 *   - On SSE event: invalidates ['list', listId] → background refetch (D-10)
 *   - LiveSyncIndicator renders connection status in the header
 *   - refetchInterval:30000 polling fallback always active (D-12)
 *   - Note: SSE connection lives in ListDetail (one stream per list navigation).
 *     A future optimization could hoist this to the Lists route level — acceptable
 *     for Phase 4 per RESEARCH note; Phase 5 push will own the session lifecycle.
 *
 * Security: item text rendered as plain-text JSX children (T-04-06 XSS guard).
 */

import { useState } from 'react';
import { useParams, useNavigate } from 'react-router';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { ChevronLeft } from 'lucide-react';
import {
  DndContext,
  closestCenter,
  KeyboardSensor,
  PointerSensor,
  TouchSensor,
  useSensors,
  useSensor,
  type DragEndEvent,
} from '@dnd-kit/core';
import {
  SortableContext,
  sortableKeyboardCoordinates,
  verticalListSortingStrategy,
} from '@dnd-kit/sortable';
import { generateKeyBetween } from 'fractional-indexing';
import { fetchListItems, addItem, patchListItem, deleteItem } from '../api/listsClient.js';
import { ItemRow } from '../components/ItemRow.js';
import { AddItemInput } from '../components/AddItemInput.js';
import { LiveSyncIndicator } from '../components/LiveSyncIndicator.js';
import { useListSSE } from '../hooks/useListSSE.js';
import type { SyncState } from '../hooks/useListSSE.js';
import type { ListItem, ListItemsResponse } from '../api/listsClient.js';

/**
 * ListEmptyState — shown inside ListDetail when list has no items.
 */
function ListEmptyState() {
  return (
    <div
      style={{
        display: 'flex',
        flexDirection: 'column',
        alignItems: 'center',
        justifyContent: 'center',
        flex: 1,
        gap: 'var(--space-3)',
        padding: 'var(--space-6) var(--space-4)',
        textAlign: 'center',
        fontFamily: 'var(--font-family-base)',
      }}
      aria-live="polite"
    >
      <div
        style={{
          fontSize: 'var(--text-heading-size, 18px)',
          fontWeight: 600,
          color: 'var(--color-text-primary)',
        }}
      >
        Nothing here yet
      </div>
      <div
        style={{
          fontSize: 'var(--text-body-size, 15px)',
          color: 'var(--color-text-muted)',
          maxWidth: '240px',
        }}
      >
        Add your first item below.
      </div>
    </div>
  );
}

export function ListDetail() {
  const { listId } = useParams<{ listId: string }>();
  const navigate = useNavigate();
  const queryClient = useQueryClient();
  const parsedListId = Number(listId);

  const [completedExpanded, setCompletedExpanded] = useState(true);
  const [syncState, setSyncState] = useState<SyncState>('connected');

  // ── Live sync (LIST-04) ──────────────────────────────────────────────────
  // Bounded-backoff SSE hook (D-11). On open: invalidates ['list', listId] (D-10).
  // On event: invalidates ['list', listId] → background refetch.
  // Polling fallback: refetchInterval:30000 below stays always active (D-12).
  useListSSE({ listId: parsedListId, onStateChange: setSyncState });

  // ── dnd-kit sensors ────────────────────────────────────────────────────────
  // PointerSensor: desktop mouse — immediate activation
  // TouchSensor: mobile touch — 200ms long-press + 5px tolerance to avoid
  //   accidental drags while scrolling (no accidental drags constraint)
  // KeyboardSensor: accessibility keyboard-reorder fallback (WCAG)
  const sensors = useSensors(
    useSensor(PointerSensor),
    useSensor(TouchSensor, {
      activationConstraint: {
        delay: 200,
        tolerance: 5,
      },
    }),
    useSensor(KeyboardSensor, {
      coordinateGetter: sortableKeyboardCoordinates,
    }),
  );

  // ── Data fetch ─────────────────────────────────────────────────────────────
  // refetchInterval: 30000 = D-12 polling fallback (always active; SSE layered in Plan 06)
  const { data, isLoading, isError } = useQuery({
    queryKey: ['list', parsedListId],
    queryFn: () => fetchListItems(parsedListId),
    refetchInterval: 30_000,
    enabled: !isNaN(parsedListId),
  });

  // Split into active (unchecked, sorted rank ASC) and completed (checked) per D-05
  const allItems: ListItem[] = data?.items ?? [];
  const activeItems = allItems
    .filter((i) => !i.checked)
    .sort((a, b) => (a.rank < b.rank ? -1 : a.rank > b.rank ? 1 : 0));
  const completedItems = allItems.filter((i) => i.checked);

  // ── Add item mutation (D-07 optimistic: append at active bottom, opacity 0.6) ──
  const addMutation = useMutation({
    mutationFn: (text: string) => addItem(parsedListId, { text }),
    onMutate: async (text: string) => {
      await queryClient.cancelQueries({ queryKey: ['list', parsedListId] });
      const previous = queryClient.getQueryData<ListItemsResponse>(['list', parsedListId]);

      // Compute optimistic rank: after the last active item
      const lastRank = activeItems.at(-1)?.rank ?? null;
      const optimisticRank = generateKeyBetween(lastRank, null);

      const optimisticItem: ListItem = {
        id: -Date.now(), // temporary negative id
        listId: parsedListId,
        text,
        checked: false,
        rank: optimisticRank,
      };

      queryClient.setQueryData<ListItemsResponse>(['list', parsedListId], (old) => ({
        items: [...(old?.items ?? []), optimisticItem],
      }));

      return { previous, optimisticItem };
    },
    onError: (_err, _text, context) => {
      // Rollback on rejection
      if (context?.previous) {
        queryClient.setQueryData(['list', parsedListId], context.previous);
      }
    },
    onSettled: () => {
      // fire-and-forget: cache invalidation; React Query handles the refetch lifecycle
      void queryClient.invalidateQueries({ queryKey: ['list', parsedListId] });
    },
  });

  // ── Check/uncheck mutation (D-07 optimistic: move section immediately, rollback on error) ──
  const checkMutation = useMutation({
    mutationFn: ({ itemId, checked }: { itemId: number; checked: boolean }) =>
      patchListItem(itemId, { checked }),
    onMutate: async ({ itemId, checked }) => {
      await queryClient.cancelQueries({ queryKey: ['list', parsedListId] });
      const previous = queryClient.getQueryData<ListItemsResponse>(['list', parsedListId]);

      queryClient.setQueryData<ListItemsResponse>(['list', parsedListId], (old) => ({
        items: (old?.items ?? []).map((item) => (item.id === itemId ? { ...item, checked } : item)),
      }));

      return { previous };
    },
    onError: (_err, _vars, context) => {
      if (context?.previous) {
        queryClient.setQueryData(['list', parsedListId], context.previous);
      }
    },
    onSettled: () => {
      // fire-and-forget: cache invalidation; React Query handles the refetch lifecycle
      void queryClient.invalidateQueries({ queryKey: ['list', parsedListId] });
    },
  });

  // ── Delete item mutation (D-09 delete-wins: NO rollback on error) ──
  const deleteMutation = useMutation({
    mutationFn: (itemId: number) => deleteItem(itemId),
    onMutate: async (itemId: number) => {
      await queryClient.cancelQueries({ queryKey: ['list', parsedListId] });
      // Remove item optimistically — no previous state saved (delete-wins D-09)
      queryClient.setQueryData<ListItemsResponse>(['list', parsedListId], (old) => ({
        items: (old?.items ?? []).filter((item) => item.id !== itemId),
      }));
    },
    // No onError rollback — delete-wins (D-09)
    onSettled: () => {
      // fire-and-forget: cache invalidation; React Query handles the refetch lifecycle
      void queryClient.invalidateQueries({ queryKey: ['list', parsedListId] });
    },
  });

  // ── Reorder mutation (D-13 single-row rank write, D-15 LWW convergence) ──
  const reorderMutation = useMutation({
    mutationFn: ({ itemId, position }: { itemId: number; position: string }) =>
      patchListItem(itemId, { position }),
    onMutate: async ({ itemId, position }) => {
      await queryClient.cancelQueries({ queryKey: ['list', parsedListId] });
      const previous = queryClient.getQueryData<ListItemsResponse>(['list', parsedListId]);

      // Optimistic: update the moved item's rank in cache immediately
      queryClient.setQueryData<ListItemsResponse>(['list', parsedListId], (old) => ({
        items: (old?.items ?? []).map((item) =>
          item.id === itemId ? { ...item, rank: position } : item,
        ),
      }));

      return { previous };
    },
    onError: (_err, _vars, context) => {
      // Rollback: animate back to previous order (D-14 — CSS transition in ItemRow handles animation)
      if (context?.previous) {
        queryClient.setQueryData(['list', parsedListId], context.previous);
      }
    },
    onSettled: () => {
      // fire-and-forget: cache invalidation; React Query handles the refetch lifecycle
      void queryClient.invalidateQueries({ queryKey: ['list', parsedListId] });
    },
  });

  // ── Handlers ───────────────────────────────────────────────────────────────

  function handleAdd(text: string) {
    addMutation.mutate(text);
  }

  function handleCheck(itemId: number, checked: boolean) {
    checkMutation.mutate({ itemId, checked });
  }

  function handleDelete(itemId: number) {
    deleteMutation.mutate(itemId);
  }

  /**
   * onDragEnd: compute new fractional rank and issue a single-item PATCH (D-13).
   *
   * Algorithm:
   *   1. Find the dragged item's current position in activeItems.
   *   2. Find the target position (over.id).
   *   3. Derive prevRank/nextRank from the active list at the destination.
   *   4. Compute newRank = generateKeyBetween(prevRank, nextRank).
   *   5. Optimistically update cache, then PATCH { position: newRank }.
   *      Only the moved item's rank is written (one-row PATCH — D-13).
   *
   * Concurrent reorder of the same item from another member: server LWW on
   * updatedAt resolves to the last write — both clients converge within ~1s
   * via SSE (Plan 06) or the 30s polling fallback (D-12, D-15).
   */
  function handleDragEnd(event: DragEndEvent) {
    const { active, over } = event;

    // No-op: dropped outside or on itself
    if (!over || active.id === over.id) return;

    // Find source and destination indices in the sorted active list
    const activeIndex = activeItems.findIndex((i) => i.id === active.id);
    const overIndex = activeItems.findIndex((i) => i.id === over.id);

    if (activeIndex === -1 || overIndex === -1) return;

    // Build the new order by moving the item to the target position
    const newOrder = [...activeItems];
    const [moved] = newOrder.splice(activeIndex, 1);
    newOrder.splice(overIndex, 0, moved);

    // Derive prevRank/nextRank from the reordered list at the moved item's new position
    const prevRank = newOrder[overIndex - 1]?.rank ?? null;
    const nextRank = newOrder[overIndex + 1]?.rank ?? null;

    const newRank = generateKeyBetween(prevRank, nextRank);

    reorderMutation.mutate({ itemId: Number(active.id), position: newRank });
  }

  // ── Loading / error states ─────────────────────────────────────────────────

  if (isNaN(parsedListId)) {
    return (
      <div
        style={{
          padding: 'var(--space-4)',
          fontFamily: 'var(--font-family-base)',
          color: 'var(--color-text-primary)',
        }}
      >
        Invalid list.
      </div>
    );
  }

  // ── Render ─────────────────────────────────────────────────────────────────

  return (
    <div
      style={{
        display: 'flex',
        flexDirection: 'column',
        height: '100%',
        background: 'var(--color-surface)',
        fontFamily: 'var(--font-family-base)',
        paddingBottom: 'calc(56px + env(safe-area-inset-bottom, 0px))',
      }}
    >
      {/* Header */}
      <div
        style={{
          display: 'flex',
          alignItems: 'center',
          gap: 'var(--space-2)',
          padding: 'var(--space-3) var(--space-4)',
          borderBottom: '1px solid var(--color-border)',
          minHeight: '56px',
        }}
      >
        <button
          onClick={() => {
            void navigate('/lists');
          }}
          aria-label="Back to lists"
          style={{
            background: 'none',
            border: 'none',
            cursor: 'pointer',
            padding: 'var(--space-2)',
            color: 'var(--color-text-primary)',
            display: 'flex',
            alignItems: 'center',
            minWidth: '44px',
            minHeight: '44px',
          }}
        >
          <ChevronLeft size={20} aria-hidden="true" />
        </button>
        <h1
          style={{
            flex: 1,
            fontSize: 'var(--text-heading-size, 18px)',
            fontWeight: 600,
            color: 'var(--color-text-primary)',
            margin: 0,
          }}
        >
          {/* List name not available without an extra fetch; shown as placeholder.
              Plan 05/06 can enrich from the ['lists'] cache by listId. */}
          List
        </h1>

        {/* Live sync indicator — connected/reconnecting/disconnected (LIST-04, D-11) */}
        <LiveSyncIndicator state={syncState} />
      </div>

      {/* Content area */}
      <div
        style={{
          flex: 1,
          overflowY: 'auto',
          display: 'flex',
          flexDirection: 'column',
        }}
      >
        {isLoading && (
          <div
            style={{
              padding: 'var(--space-6) var(--space-4)',
              color: 'var(--color-text-muted)',
              textAlign: 'center',
              fontSize: 'var(--text-body-size, 15px)',
            }}
          >
            Loading…
          </div>
        )}

        {isError && (
          <div
            style={{
              padding: 'var(--space-6) var(--space-4)',
              color: 'var(--color-destructive)',
              textAlign: 'center',
              fontSize: 'var(--text-body-size, 15px)',
            }}
          >
            Failed to load items.
          </div>
        )}

        {!isLoading && !isError && allItems.length === 0 && <ListEmptyState />}

        {/* Active items section — wrapped in DndContext + SortableContext (LIST-03) */}
        {activeItems.length > 0 && (
          <DndContext
            sensors={sensors}
            collisionDetection={closestCenter}
            onDragEnd={handleDragEnd}
          >
            <SortableContext
              items={activeItems.map((i) => i.id)}
              strategy={verticalListSortingStrategy}
            >
              <div role="list" aria-label="Active items">
                {activeItems.map((item) => (
                  <div key={item.id} role="listitem">
                    <ItemRow
                      item={item}
                      isActive={true}
                      onCheck={handleCheck}
                      onDelete={handleDelete}
                      // Newly optimistic items have negative id and dim opacity
                      optimisticOpacity={item.id < 0 ? 0.6 : 1}
                    />
                  </div>
                ))}
              </div>
            </SortableContext>
          </DndContext>
        )}

        {/* Completed section (D-05) — collapsible, default expanded */}
        {completedItems.length > 0 && (
          <div>
            <button
              onClick={() => setCompletedExpanded((prev) => !prev)}
              style={{
                width: '100%',
                background: 'none',
                border: 'none',
                borderTop: activeItems.length > 0 ? '1px solid var(--color-border)' : 'none',
                padding: 'var(--space-3) var(--space-4)',
                display: 'flex',
                alignItems: 'center',
                cursor: 'pointer',
                color: 'var(--color-text-muted)',
                fontSize: 'var(--text-label-size, 13px)',
                fontWeight: 400,
                textAlign: 'left',
                minHeight: '44px',
              }}
              aria-expanded={completedExpanded}
            >
              Completed ({completedItems.length})
            </button>

            {completedExpanded && (
              <div role="list" aria-label="Completed items">
                {completedItems.map((item) => (
                  <div key={item.id} role="listitem">
                    <ItemRow
                      item={item}
                      isActive={false}
                      onCheck={handleCheck}
                      onDelete={handleDelete}
                    />
                  </div>
                ))}
              </div>
            )}
          </div>
        )}
      </div>

      {/* Sticky add-item input */}
      <AddItemInput onAdd={handleAdd} isPending={addMutation.isPending} />
    </div>
  );
}
