import { useEffect, useRef, useState } from 'react';
import { Link, useNavigate, useSearchParams } from 'react-router';
import { useQuery, useQueryClient, keepPreviousData } from '@tanstack/react-query';
import {
  ChevronLeft,
  ChevronRight,
  Inbox,
  Loader2,
  Mail,
  Paperclip,
  RefreshCw,
  Search,
  Send,
  ShieldAlert,
  SquarePen,
  Trash2,
  X,
} from 'lucide-react';
import { toast } from 'sonner';
import { Button } from '@/components/ui/button';
import { useClient } from 'hooks/useClient';
import type { EmailSummary } from 'types';
import { useComposer } from './Compose';
import { emailPath, useSelectedEmailId } from './shared';

type EmailAccountRow = {
  id: number;
  provider: string;
  email: string;
  status: string;
};

const LIMIT = 50;

const FOLDERS = [
  { key: 'inbox', label: 'Inbox', icon: Inbox },
  { key: 'all', label: 'All', icon: Mail },
  { key: 'sent', label: 'Sent', icon: Send },
  { key: 'spam', label: 'Spam', icon: ShieldAlert },
  { key: 'trash', label: 'Trash', icon: Trash2 },
] as const;

const formatDate = (iso: string) => {
  const date = new Date(iso);
  const now = new Date();
  const isToday = date.toDateString() === now.toDateString();
  if (isToday) return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
  const isThisYear = date.getFullYear() === now.getFullYear();
  if (isThisYear) return date.toLocaleDateString([], { month: 'short', day: 'numeric' });
  return date.toLocaleDateString([], { month: 'short', day: 'numeric', year: 'numeric' });
};

export const EmailList = () => {
  const client = useClient();
  const queryClient = useQueryClient();
  const [, openCompose] = useComposer();
  const navigate = useNavigate();
  const selectedId = useSelectedEmailId();
  // Which folder you are reading is addressable state, so it is a query param rather than a global.
  // It stays a *param* and not a path segment because the path already spells which email is open:
  // `/email/:emailId` is the thing, the folder is the view you found it through, and switching folders
  // with a message open should not close it.
  const [searchParams, setSearchParams] = useSearchParams();
  const folder = searchParams.get('folder') ?? 'inbox';
  const qs = searchParams.toString() ? `?${searchParams}` : '';
  const [page, setPage] = useState(1);
  const [search, setSearch] = useState('');
  const [debouncedSearch, setDebouncedSearch] = useState('');
  const isSearching = debouncedSearch.length > 0;
  useEffect(() => {
    const t = setTimeout(() => setDebouncedSearch(search.trim()), 300);
    return () => clearTimeout(t);
  }, [search]);
  // Page 1 on a new search *or* a new folder. The folder half used to live in the click handler; it
  // cannot any more, because the folder pills are links and nobody handles their click.
  useEffect(() => {
    setPage(1);
  }, [debouncedSearch, folder]);

  const { data: emailAccounts = [], refetch: refetchAccounts } = useQuery({
    queryKey: ['email-accounts'],
    queryFn: () => client.get<EmailAccountRow[]>('/email/accounts'),
  });
  const syncableAccount = emailAccounts.find((a) => a.status === 'connected' || a.status === 'synced');
  const isSyncing = emailAccounts.some((a) => a.status === 'syncing' || a.status === 'queued');
  const hasAccounts = emailAccounts.length > 0;

  const { data, isLoading } = useQuery({
    queryKey: isSearching ? ['email-search', debouncedSearch, page] : ['email-messages', page, folder],
    queryFn: () =>
      isSearching
        ? client.get<{ messages: EmailSummary[]; total: number }>(
            `/email/search?q=${encodeURIComponent(debouncedSearch)}&page=${page}&limit=${LIMIT}`,
          )
        : client.get<{ messages: EmailSummary[]; total: number }>(
            `/email/messages?page=${page}&limit=${LIMIT}&folder=${folder}`,
          ),
    // Keep the previous results visible while the next query loads, so switching search terms/pages
    // never bails to the full-panel loading view (which would unmount the search input and drop focus).
    placeholderData: keepPreviousData,
  });

  // Auto-switch to "all" if inbox filter returns nothing but emails exist (not while searching)
  const { data: allCount } = useQuery({
    queryKey: ['email-messages-all-count'],
    queryFn: () => client.get<{ messages: EmailSummary[]; total: number }>('/email/messages?page=1&limit=1&folder=all'),
    enabled: !isSearching && folder === 'inbox' && !isLoading && (data?.total ?? 0) === 0,
  });

  useEffect(() => {
    if (!isSearching && folder === 'inbox' && data?.total === 0 && allCount && allCount.total > 0) {
      // `replace`: this is the app correcting its own default, not a place you chose to be, so Back
      // should leave /email rather than bounce you between inbox and all.
      setSearchParams(
        (prev) => {
          const next = new URLSearchParams(prev);
          next.set('folder', 'all');
          return next;
        },
        { replace: true },
      );
    }
  }, [isSearching, folder, data?.total, allCount, setSearchParams]);

  // Live updates: while /email is open, listen for new-mail pushes (IMAP IDLE → SSE) and refetch.
  // The EventSource closes automatically when this component unmounts (i.e. when you leave /email).
  useEffect(() => {
    const token = localStorage.getItem('BEARER_TOKEN');
    if (!token) return;
    const es = new EventSource(`/api/email/events?token=${encodeURIComponent(token)}`);
    es.onmessage = (ev) => {
      try {
        if ((JSON.parse(ev.data) as { type?: string }).type === 'new-mail') {
          queryClient.invalidateQueries({ queryKey: ['email-messages'] });
          queryClient.invalidateQueries({ queryKey: ['email-messages-all-count'] });
          queryClient.invalidateQueries({ queryKey: ['email-accounts'] });
        }
      } catch {
        /* ignore non-JSON keepalives */
      }
    };
    return () => es.close();
  }, [queryClient]);

  const [syncing, setSyncing] = useState(false);

  const handleSync = async () => {
    if (!syncableAccount) return;
    setSyncing(true);
    try {
      const result = await client.post<{ ok: boolean; saved?: number }>(
        `/email/accounts/${syncableAccount.id}/sync`,
        {},
      );
      if (result.saved !== undefined) {
        toast.success(result.saved > 0 ? `${result.saved} new emails` : 'No new emails');
        queryClient.invalidateQueries({ queryKey: ['email-messages'] });
      } else {
        toast.success('Sync started');
      }
      refetchAccounts();
    } catch (err) {
      toast.error(err instanceof Error ? err.message : 'Failed to sync');
    } finally {
      setSyncing(false);
    }
  };

  // Poll accounts while first sync is running (job-based), refresh email list when done
  const prevSyncing = useRef(false);
  useEffect(() => {
    if (isSyncing) {
      const interval = setInterval(refetchAccounts, 5000);
      return () => clearInterval(interval);
    }
    if (prevSyncing.current && !isSyncing) {
      queryClient.invalidateQueries({ queryKey: ['email-messages'] });
    }
    prevSyncing.current = isSyncing;
  }, [isSyncing, queryClient]);

  const messages = data?.messages ?? [];
  const total = data?.total ?? 0;
  const totalPages = Math.max(1, Math.ceil(total / LIMIT));

  // Arrow key navigation
  useEffect(() => {
    const handleKeyDown = (ev: KeyboardEvent) => {
      if (ev.key !== 'ArrowUp' && ev.key !== 'ArrowDown') return;
      if (messages.length === 0) return;
      ev.preventDefault();
      const currentIndex = selectedId ? messages.findIndex((m) => m.id === selectedId) : -1;
      let nextId: string | undefined;
      if (ev.key === 'ArrowDown') {
        const next = currentIndex + 1;
        if (next < messages.length) nextId = messages[next]!.id;
      } else {
        const prev = currentIndex - 1;
        if (prev >= 0) nextId = messages[prev]!.id;
      }
      if (nextId) {
        // `replace` on purpose: sweeping down a folder with the arrow keys would otherwise stack one
        // history entry per row, and Back would then walk the sweep instead of leaving the mailbox.
        // A click is a real link and does push.
        navigate(emailPath(nextId, qs), { replace: true });
        document.querySelector(`[data-email-id="${nextId}"]`)?.scrollIntoView({ block: 'nearest' });
      }
    };
    window.addEventListener('keydown', handleKeyDown);
    return () => window.removeEventListener('keydown', handleKeyDown);
  }, [messages, selectedId, navigate, qs]);

  if (isLoading) {
    return <div className="flex h-full items-center justify-center text-sm opacity-50">Loading emails...</div>;
  }

  // Show onboarding empty state only when no emails exist at all (never while searching)
  const hasNoEmails = !isSearching && total === 0 && folder === 'inbox' && !allCount?.total;
  if (hasNoEmails && !isLoading && page === 1) {
    return (
      <div className="flex h-full flex-col items-center justify-center gap-3 text-sm opacity-50">
        <Mail className="h-8 w-8" />
        {!hasAccounts ? (
          <div className="flex flex-col items-center gap-2">
            <span>Add an email account to get started</span>
            <Button variant="outline" size="sm" asChild>
              <Link to="/settings/integrations">Add Account</Link>
            </Button>
          </div>
        ) : syncableAccount ? (
          <div className="flex flex-col items-center gap-2">
            <span>No emails synced yet</span>
            <Button variant="outline" size="sm" onClick={handleSync} disabled={syncing || isSyncing}>
              {syncing || isSyncing ? (
                <Loader2 className="mr-2 h-3.5 w-3.5 animate-spin" />
              ) : (
                <RefreshCw className="mr-2 h-3.5 w-3.5" />
              )}
              Sync Now
            </Button>
          </div>
        ) : isSyncing ? (
          <div className="flex flex-col items-center gap-2">
            <Loader2 className="h-5 w-5 animate-spin" />
            <span>Syncing emails...</span>
          </div>
        ) : (
          <span>No emails synced yet</span>
        )}
      </div>
    );
  }

  return (
    <div className="flex h-full flex-col">
      <div className="flex shrink-0 items-center gap-2 border-b px-3 py-2">
        <div className="flex items-center gap-1">
          {FOLDERS.map((f) => {
            const Icon = f.icon;
            const isActive = folder === f.key;
            // Search-only `to`, so react-router keeps the current pathname — the open email survives a
            // folder switch. Built from the live params rather than written flat, so anything else in
            // the query string comes along.
            const next = new URLSearchParams(searchParams);
            next.set('folder', f.key);
            return (
              <Link
                key={f.key}
                to={{ search: `?${next}` }}
                className={`flex items-center gap-1 rounded px-2 py-1 text-xs transition-colors cursor-pointer ${
                  isActive ? 'bg-accent font-medium' : 'opacity-60 hover:opacity-100 hover:bg-accent/50'
                }`}
                title={f.label}
                aria-current={isActive ? 'true' : undefined}
              >
                <Icon className="h-3.5 w-3.5" />
                <span className="hidden sm:inline">{f.label}</span>
              </Link>
            );
          })}
        </div>
        <span className="text-xs opacity-50">{total}</span>
        {syncing || isSyncing ? (
          <span title="Syncing..." className="flex shrink-0">
            <Loader2 className="h-3.5 w-3.5 animate-spin opacity-50" />
          </span>
        ) : syncableAccount ? (
          <button
            onClick={handleSync}
            className="p-0.5 rounded hover:bg-black/10 transition-colors cursor-pointer shrink-0"
            title="Sync emails"
          >
            <RefreshCw className="h-3.5 w-3.5" />
          </button>
        ) : null}
        <button
          onClick={() => openCompose({})}
          className="p-0.5 rounded hover:bg-black/10 transition-colors cursor-pointer shrink-0"
          title="Compose"
        >
          <SquarePen className="h-3.5 w-3.5" />
        </button>
        {totalPages > 1 && (
          <div className="ml-auto flex items-center gap-2">
            <button
              onClick={() => setPage((p) => Math.max(1, p - 1))}
              disabled={page <= 1}
              className="flex items-center text-xs disabled:opacity-30 cursor-pointer disabled:cursor-default"
            >
              <ChevronLeft className="h-3.5 w-3.5" />
            </button>
            <span className="text-xs opacity-60">
              {page}/{totalPages}
            </span>
            <button
              onClick={() => setPage((p) => Math.min(totalPages, p + 1))}
              disabled={page >= totalPages}
              className="flex items-center text-xs disabled:opacity-30 cursor-pointer disabled:cursor-default"
            >
              <ChevronRight className="h-3.5 w-3.5" />
            </button>
          </div>
        )}
      </div>

      <div className="flex shrink-0 items-center gap-2 border-b px-3 py-1.5">
        <Search className="h-3.5 w-3.5 shrink-0 opacity-40" />
        <input
          value={search}
          onChange={(ev) => setSearch(ev.target.value)}
          placeholder="Search — try from:, subject:, has:attachment…"
          className="flex-1 bg-transparent text-sm outline-none placeholder:opacity-40"
        />
        {isSearching && (
          <span className="text-xs opacity-50 shrink-0">
            {total} result{total === 1 ? '' : 's'}
          </span>
        )}
        {search && (
          <button
            onClick={() => setSearch('')}
            className="shrink-0 opacity-40 hover:opacity-100 cursor-pointer"
            title="Clear search"
          >
            <X className="h-3.5 w-3.5" />
          </button>
        )}
      </div>
      {messages.length === 0 ? (
        <div className="flex flex-1 items-center justify-center text-sm opacity-40">
          {isSearching ? `No results for “${debouncedSearch}”` : 'No emails in this folder'}
        </div>
      ) : (
        <div className="flex flex-1 flex-col divide-y divide-white/10 overflow-y-auto">
          {messages.map((msg: EmailSummary) => {
            const unread = msg.threadUnread !== undefined ? msg.threadUnread > 0 : !msg.read;
            return (
              <Link
                key={msg.id}
                to={emailPath(msg.id, qs)}
                data-email-id={msg.id}
                className={`flex flex-col gap-0.5 px-3 py-2.5 text-left transition-colors cursor-pointer shrink-0 ${
                  selectedId === msg.id ? 'bg-accent' : 'hover:bg-accent/50'
                }`}
              >
                <div className="flex items-center justify-between gap-2">
                  <div className="flex min-w-0 items-center gap-1.5">
                    <span className={`truncate text-sm ${unread ? 'font-semibold' : 'font-medium opacity-70'}`}>
                      {msg.from}
                    </span>
                    {!!msg.threadCount && (
                      <span
                        className="shrink-0 rounded-full bg-muted px-1.5 text-xs tabular-nums opacity-60"
                        title={`${msg.threadCount} messages`}
                      >
                        {msg.threadCount}
                      </span>
                    )}
                  </div>
                  <span className="shrink-0 text-xs opacity-50">{formatDate(msg.date)}</span>
                </div>
                <span className={`truncate text-sm ${unread ? 'font-medium' : 'opacity-70'}`}>{msg.subject}</span>
                <div className="flex items-center gap-1.5 text-xs">
                  {!!msg.attachmentCount && (
                    <span className="flex shrink-0 items-center gap-0.5 text-muted-foreground">
                      <Paperclip className="h-3 w-3" />
                      {msg.attachmentCount}
                    </span>
                  )}
                  <span className="truncate opacity-50">{msg.snippet}</span>
                </div>
              </Link>
            );
          })}
        </div>
      )}
    </div>
  );
};
