import {
  DndContext,
  KeyboardSensor,
  PointerSensor,
  closestCenter,
  useSensor,
  useSensors,
  type DragEndEvent,
} from '@dnd-kit/core';
import {
  SortableContext,
  rectSortingStrategy,
  sortableKeyboardCoordinates,
} from '@dnd-kit/sortable';
import {
  ToolboxHeader,
  type ToolboxHeaderApp,
} from '@add-ideas/toolbox-shell-react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { AppCard } from './components/AppCard';
import { AppDetailsPanel } from './components/AppDetailsPanel';
import { HelpPanel } from './components/HelpPanel';
import { Logo } from './components/Logo';
import { PreferencesPanel } from './components/PreferencesPanel';
import { contextualLaunchUrl, loadCatalogue } from './catalogue';
import { cleanPreferences, reorderWithinIds, sortAppIds } from './preferences';
import type { LoadedCatalogue, ResolvedApp } from './types';
import { usePreferences } from './usePreferences';

const CATALOGUE_HREF = './toolbox.catalog.json';
const PORTAL_SOURCE = 'https://git.add-ideas.de/zemion/toolbox-portal';

type LoadState =
  | { status: 'loading' }
  | { status: 'error'; message: string }
  | { status: 'ready'; value: LoadedCatalogue };

type PortalOverlay =
  { kind: 'help' } | { kind: 'details'; appId: string } | null;

function appMatches(
  app: ResolvedApp,
  query: string,
  category: string,
  tag: string
): boolean {
  if (category && !app.categories.includes(category)) return false;
  if (tag && !app.tags.includes(tag)) return false;
  if (!query) return true;
  const haystack = [app.name, app.description, ...app.categories, ...app.tags]
    .join(' ')
    .toLocaleLowerCase();
  return haystack.includes(query.toLocaleLowerCase());
}

interface AppSectionProps {
  apps: ResolvedApp[];
  catalogueUrl: string;
  hiddenIds: readonly string[];
  id: string;
  onDetails: (id: string) => void;
  onHide: (id: string) => void;
  onPin: (id: string) => void;
  pinned: boolean;
  title: string;
}

function AppSection({
  apps,
  catalogueUrl,
  hiddenIds,
  id,
  onDetails,
  onHide,
  onPin,
  pinned,
  title,
}: AppSectionProps) {
  if (apps.length === 0) return null;
  return (
    <section className="app-section" aria-labelledby={`${id}-title`}>
      <header className="app-section__header">
        <h2 id={`${id}-title`}>{title}</h2>
        <span>{apps.length}</span>
      </header>
      <SortableContext
        id={id}
        items={apps.map((app) => app.id)}
        strategy={rectSortingStrategy}
      >
        <div className="app-grid">
          {apps.map((app) => (
            <AppCard
              key={app.id}
              app={app}
              catalogueUrl={catalogueUrl}
              contextualUrl={contextualLaunchUrl}
              pinned={pinned}
              hidden={hiddenIds.includes(app.id)}
              onDetails={() => onDetails(app.id)}
              onPin={() => onPin(app.id)}
              onHide={() => onHide(app.id)}
            />
          ))}
        </div>
      </SortableContext>
    </section>
  );
}

export default function App() {
  const [loadState, setLoadState] = useState<LoadState>({ status: 'loading' });
  const [reloadKey, setReloadKey] = useState(0);
  const [query, setQuery] = useState('');
  const [category, setCategory] = useState('');
  const [activeTag, setActiveTag] = useState('');
  const [shownHiddenIds, setShownHiddenIds] = useState<
    readonly string[] | null
  >(null);
  const [overlay, setOverlay] = useState<PortalOverlay>(null);
  const hiddenToggle = useRef<HTMLInputElement>(null);
  const [preferences, setPreferences, storageAvailable] = usePreferences();
  const closeOverlay = useCallback(() => setOverlay(null), []);
  const showHidden =
    preferences.hidden.length > 0 &&
    shownHiddenIds?.some((id) => preferences.hidden.includes(id)) === true;
  const sensors = useSensors(
    useSensor(PointerSensor, { activationConstraint: { distance: 8 } }),
    useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates })
  );

  useEffect(() => {
    const controller = new AbortController();
    loadCatalogue(CATALOGUE_HREF, controller.signal).then(
      (value) => setLoadState({ status: 'ready', value }),
      (error) => {
        if (controller.signal.aborted) return;
        setLoadState({
          status: 'error',
          message:
            error instanceof Error
              ? error.message
              : 'The catalogue could not be loaded.',
        });
      }
    );
    return () => controller.abort();
  }, [reloadKey]);

  const loaded = loadState.status === 'ready' ? loadState.value : undefined;
  const apps = useMemo(() => loaded?.apps ?? [], [loaded]);
  const detailsApp =
    overlay?.kind === 'details'
      ? apps.find((app) => app.id === overlay.appId)
      : undefined;
  const overlayOpen = overlay?.kind === 'help' || detailsApp !== undefined;

  useEffect(() => {
    if (!loaded) return;
    const clean = cleanPreferences(
      preferences,
      loaded.apps.map((app) => app.id)
    );
    if (JSON.stringify(clean) !== JSON.stringify(preferences))
      setPreferences(clean);
  }, [loaded, preferences, setPreferences]);

  useEffect(() => {
    document.documentElement.dataset.theme = preferences.theme;
  }, [preferences.theme]);

  const categories = useMemo(
    () =>
      [...new Set(apps.flatMap((app) => app.categories))].sort((a, b) =>
        a.localeCompare(b)
      ),
    [apps]
  );
  const orderedApps = useMemo(() => {
    const byId = new Map(apps.map((app) => [app.id, app]));
    return sortAppIds(
      apps.map((app) => app.id),
      preferences
    ).flatMap((id) => {
      const app = byId.get(id);
      return app ? [app] : [];
    });
  }, [apps, preferences]);
  const visibleApps = orderedApps.filter((app) => {
    const hidden = preferences.hidden.includes(app.id);
    return (
      (showHidden || !hidden) &&
      appMatches(app, query.trim(), category, activeTag)
    );
  });
  const pinnedApps = visibleApps.filter((app) =>
    preferences.pinned.includes(app.id)
  );
  const regularApps = visibleApps.filter(
    (app) => !preferences.pinned.includes(app.id)
  );

  const headerApps: ToolboxHeaderApp[] = apps.map((app) => ({
    id: app.id,
    name: app.name,
    href: app.isExternal
      ? app.entryUrl
      : contextualLaunchUrl(app.entryUrl, loaded?.catalogueUrl.href ?? ''),
    newTab: !app.launchModes.includes('navigate'),
  }));

  function toggleListItem(field: 'pinned' | 'hidden', id: string) {
    const keepCardVisible = field === 'pinned' || showHidden;
    if (field === 'hidden' && showHidden && !preferences.hidden.includes(id)) {
      setShownHiddenIds((current) => [...new Set([...(current ?? []), id])]);
    }
    setPreferences((current) => ({
      ...current,
      [field]: current[field].includes(id)
        ? current[field].filter((item) => item !== id)
        : [...current[field], id],
    }));
    window.requestAnimationFrame(() => {
      if (!keepCardVisible) {
        hiddenToggle.current?.focus();
        return;
      }
      const action = field === 'pinned' ? 'pin' : 'visibility';
      [
        ...document.querySelectorAll<HTMLButtonElement>(
          `[data-card-action="${action}"]`
        ),
      ]
        .find((button) => button.dataset.appId === id)
        ?.focus();
    });
  }

  function handleDragEnd(event: DragEndEvent) {
    if (!event.over) return;
    const activeId = String(event.active.id);
    const overId = String(event.over.id);
    const activePinned = preferences.pinned.includes(activeId);
    if (activePinned !== preferences.pinned.includes(overId)) return;
    const peers = (activePinned ? pinnedApps : regularApps).map(
      (app) => app.id
    );
    setPreferences((current) => ({
      ...current,
      order: reorderWithinIds(current.order, activeId, overId, peers),
    }));
  }

  function clearFilters() {
    setQuery('');
    setCategory('');
    setActiveTag('');
    setShownHiddenIds(null);
  }

  return (
    <>
      <div
        className="site-shell"
        inert={overlayOpen}
        aria-hidden={overlayOpen || undefined}
      >
        <a className="skip-link" href="#main-content">
          Skip to tools
        </a>
        <ToolboxHeader
          className="portal-header"
          title="Toolbox"
          subtitle="Local-first browser tools"
          theme={preferences.theme}
          homeHref={loaded?.homeUrl ?? './'}
          homeLabel="add·ideas Toolbox"
          brandIconUrl="./favicon.svg"
          helpAction={{ onClick: () => setOverlay({ kind: 'help' }) }}
          sourceHref={PORTAL_SOURCE}
          sourceLabel="Toolbox source on Gitea"
          personalizeContent={
            <PreferencesPanel
              preferences={preferences}
              storageAvailable={storageAvailable}
              onChange={setPreferences}
            />
          }
          apps={headerApps}
          allAppsHref="#main-content"
        />

        <main id="main-content">
          <section className="hero" aria-labelledby="page-title">
            <div className="hero-intro">
              <p className="eyebrow">
                Privacy details up front · useful by default
              </p>
              <h2 id="page-title">
                Your document tools,
                <br /> <span>in one calm place.</span>
              </h2>
              <p className="hero-copy">
                Focused browser utilities with a privacy disclosure for each
                tool. Choose one and keep your work moving.
              </p>
            </div>
          </section>

          {loaded && (
            <section className="tool-controls" aria-label="Find a tool">
              <label className="search-field">
                <span className="visually-hidden">Search tools</span>
                <span aria-hidden="true">⌕</span>
                <input
                  value={query}
                  onChange={(event) => setQuery(event.target.value)}
                  placeholder="Search tools"
                  type="search"
                />
              </label>
              <label className="category-field">
                <span className="visually-hidden">Filter by category</span>
                <select
                  value={category}
                  onChange={(event) => setCategory(event.target.value)}
                >
                  <option value="">All categories</option>
                  {categories.map((item) => (
                    <option value={item} key={item}>
                      {item}
                    </option>
                  ))}
                </select>
              </label>
              {activeTag && (
                <button
                  className="active-filter"
                  type="button"
                  onClick={() => setActiveTag('')}
                >
                  #{activeTag} <span aria-hidden="true">×</span>
                  <span className="visually-hidden">Clear tag filter</span>
                </button>
              )}
              <label
                className={`hidden-toggle${
                  preferences.hidden.length === 0
                    ? ' hidden-toggle--disabled'
                    : ''
                }`}
              >
                <input
                  ref={hiddenToggle}
                  type="checkbox"
                  checked={showHidden}
                  disabled={preferences.hidden.length === 0}
                  onChange={(event) =>
                    setShownHiddenIds(
                      event.target.checked ? [...preferences.hidden] : null
                    )
                  }
                />
                Show hidden ({preferences.hidden.length})
              </label>
            </section>
          )}

          {loadState.status === 'loading' && (
            <section className="state-card" aria-live="polite">
              <div className="loader" aria-hidden="true" />
              <h2>Loading your toolbox</h2>
              <p>Reading the local app catalogue…</p>
            </section>
          )}

          {loadState.status === 'error' && (
            <section className="state-card state-card--error" role="alert">
              <span className="state-icon" aria-hidden="true">
                !
              </span>
              <h2>Toolbox unavailable</h2>
              <p>{loadState.message}</p>
              <button
                type="button"
                onClick={() => {
                  setLoadState({ status: 'loading' });
                  setReloadKey((key) => key + 1);
                }}
              >
                Try again
              </button>
            </section>
          )}

          {loaded && (
            <>
              {apps.length === 0 && loaded.unavailable.length === 0 && (
                <section className="state-card">
                  <span className="state-icon" aria-hidden="true">
                    ◇
                  </span>
                  <h2>No tools yet</h2>
                  <p>
                    The catalogue is valid but does not contain any enabled
                    apps.
                  </p>
                </section>
              )}

              {apps.length > 0 && visibleApps.length === 0 && (
                <section className="state-card">
                  <span className="state-icon" aria-hidden="true">
                    ⌕
                  </span>
                  <h2>No matching tools</h2>
                  <p>
                    Try another search or category, or show your hidden tools.
                  </p>
                  <button type="button" onClick={clearFilters}>
                    Clear filters
                  </button>
                </section>
              )}

              {visibleApps.length > 0 && (
                <DndContext
                  sensors={sensors}
                  collisionDetection={closestCenter}
                  onDragEnd={handleDragEnd}
                >
                  <div className="app-sections" aria-label="Available tools">
                    <AppSection
                      id="pinned-apps"
                      title="Pinned"
                      apps={pinnedApps}
                      pinned
                      catalogueUrl={loaded.catalogueUrl.href}
                      hiddenIds={preferences.hidden}
                      onDetails={(id) =>
                        setOverlay({ kind: 'details', appId: id })
                      }
                      onPin={(id) => toggleListItem('pinned', id)}
                      onHide={(id) => toggleListItem('hidden', id)}
                    />
                    <AppSection
                      id="regular-apps"
                      title="Tools"
                      apps={regularApps}
                      pinned={false}
                      catalogueUrl={loaded.catalogueUrl.href}
                      hiddenIds={preferences.hidden}
                      onDetails={(id) =>
                        setOverlay({ kind: 'details', appId: id })
                      }
                      onPin={(id) => toggleListItem('pinned', id)}
                      onHide={(id) => toggleListItem('hidden', id)}
                    />
                  </div>
                </DndContext>
              )}

              {loaded.unavailable.length > 0 && (
                <details className="manifest-errors">
                  <summary>
                    {loaded.unavailable.length} configured{' '}
                    {loaded.unavailable.length === 1 ? 'tool is' : 'tools are'}{' '}
                    unavailable
                  </summary>
                  <ul>
                    {loaded.unavailable.map((app) => (
                      <li key={app.id}>
                        <strong>{app.name}</strong>: {app.reason}
                      </li>
                    ))}
                  </ul>
                </details>
              )}
            </>
          )}
        </main>

        <footer className="site-footer">
          <p>
            <Logo size={22} />{' '}
            <span>
              Privacy differs by tool. Review each disclosure before opening.
              Apps open as full pages—never embedded.
            </span>
          </p>
          <span>
            Portal v0.2.9 ·{' '}
            <a
              href="https://git.add-ideas.de/zemion/toolbox-portal/src/tag/v0.9.1"
              target="_blank"
              rel="noopener noreferrer"
            >
              Source and license
            </a>
          </span>
        </footer>
      </div>

      <HelpPanel open={overlay?.kind === 'help'} onClose={closeOverlay} />
      {detailsApp && loaded ? (
        <AppDetailsPanel
          app={detailsApp}
          catalogueUrl={loaded.catalogueUrl.href}
          contextualUrl={contextualLaunchUrl}
          pinned={preferences.pinned.includes(detailsApp.id)}
          hidden={preferences.hidden.includes(detailsApp.id)}
          activeTag={activeTag}
          onCategory={(nextCategory) => {
            setCategory(nextCategory);
            closeOverlay();
          }}
          onTag={(tag) => {
            setActiveTag((current) => (current === tag ? '' : tag));
            closeOverlay();
          }}
          onClose={closeOverlay}
        />
      ) : null}
    </>
  );
}
