import {
  Box,
  Button,
  config,
  Dialog,
  Icon,
  IconButton,
  Icons,
  Menu,
  MenuItem,
  PopOut,
  RectCords,
  Spinner,
  Text,
} from 'folds';
import { HttpApiEvent, HttpApiEventHandlerMap, MatrixClient } from 'matrix-js-sdk';
import FocusTrap from 'focus-trap-react';
import React, {
  MouseEventHandler,
  ReactNode,
  useCallback,
  useEffect,
  useRef,
  useState,
} from 'react';
import {
  clearCacheAndReload,
  clearLoginData,
  IDB_VERSION_CONFLICT,
  initClient,
  logoutClient,
  startClient,
} from '../../../client/initMatrix';
import { deleteSearchCacheDatabase } from '../../utils/searchCache';
import { clearPlaintextCaches } from '../../state/plaintextCaches';
import { SplashScreen } from '../../components/splash-screen';
import { ServerConfigsLoader } from '../../components/ServerConfigsLoader';
import { CapabilitiesProvider } from '../../hooks/useCapabilities';
import { MediaConfigProvider } from '../../hooks/useMediaConfig';
import { MatrixClientProvider } from '../../hooks/useMatrixClient';
import { SpecVersions } from './SpecVersions';
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
import { useSyncState } from '../../hooks/useSyncState';
import { stopPropagation } from '../../utils/keyboard';
import { SyncStatus } from './SyncStatus';
import { AuthMetadataProvider } from '../../hooks/useAuthMetadata';
import { getFallbackSession, removeFallbackSession } from '../../state/sessions';
import { pushSessionToSW } from '../../../sw-session';
import { revokeOidcTokens } from '../../../client/oidcLogout';
import { useSessionSync } from '../../hooks/useSessionSync';
import { installCryptoDiagLog } from '../../utils/cryptoDiagLog';
import { AutoDiscovery } from './AutoDiscovery';

// Capture-only E2EE diagnostics ring buffer (KE-1→4 signatures) — installed at
// module load so it sees crypto warnings from the very first sync. Idempotent;
// report download lives in Settings → Developer Tools → Crypto Diagnostics.
installCryptoDiagLog();

function ClientRootLoading() {
  return (
    <SplashScreen>
      <Box direction="Column" grow="Yes" alignItems="Center" justifyContent="Center" gap="400">
        <Spinner variant="Secondary" size="600" />
        <Text>Heating up</Text>
      </Box>
    </SplashScreen>
  );
}

function ClientRootOptions({ mx }: { mx?: MatrixClient }) {
  const [menuAnchor, setMenuAnchor] = useState<RectCords>();

  const handleToggle: MouseEventHandler<HTMLButtonElement> = (evt) => {
    const cords = evt.currentTarget.getBoundingClientRect();
    setMenuAnchor((currentState) => {
      if (currentState) return undefined;
      return cords;
    });
  };

  return (
    <IconButton
      style={{
        position: 'absolute',
        top: config.space.S100,
        right: config.space.S100,
      }}
      variant="Background"
      fill="None"
      onClick={handleToggle}
      aria-label="Account options"
    >
      <Icon size="200" src={Icons.VerticalDots} />
      <PopOut
        anchor={menuAnchor}
        position="Bottom"
        align="End"
        offset={6}
        content={
          <FocusTrap
            focusTrapOptions={{
              initialFocus: false,
              returnFocusOnDeactivate: false,
              onDeactivate: () => setMenuAnchor(undefined),
              clickOutsideDeactivates: true,
              isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown',
              isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp',
              escapeDeactivates: stopPropagation,
            }}
          >
            <Menu>
              <Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
                {mx && (
                  <MenuItem onClick={() => clearCacheAndReload(mx)} size="300" radii="300">
                    <Text as="span" size="T300" truncate>
                      Clear Cache and Reload
                    </Text>
                  </MenuItem>
                )}
                <MenuItem
                  onClick={() => {
                    if (mx) {
                      logoutClient(mx);
                      return;
                    }
                    clearLoginData();
                  }}
                  size="300"
                  radii="300"
                  variant="Critical"
                  fill="None"
                >
                  <Text as="span" size="T300" truncate>
                    Logout
                  </Text>
                </MenuItem>
              </Box>
            </Menu>
          </FocusTrap>
        }
      />
    </IconButton>
  );
}

const useLogoutListener = (mx?: MatrixClient) => {
  useEffect(() => {
    const handleLogout: HttpApiEventHandlerMap[HttpApiEvent.SessionLoggedOut] = async () => {
      // Clear the SW's cached bearer token so it stops attaching the now-revoked
      // token to media fetches (mirrors the manual logoutClient path).
      pushSessionToSW();
      mx?.stopClient();
      // Best-effort issuer revocation for OIDC sessions (the token is already
      // server-revoked here, but revoke the refresh token too). Before we drop
      // the stored session below.
      const loggedOutSession = getFallbackSession();
      if (loggedOutSession?.oidc) {
        await revokeOidcTokens(loggedOutSession).catch(() => undefined);
      }
      await mx?.clearStores();
      // The opt-in local search index holds DECRYPTED message plaintext. Wipe it
      // on server-forced logout too (token expiry / remote sign-out / password
      // change) — the manual logout path already does, but this path didn't, so
      // the plaintext survived on disk (and persist() makes it non-evictable).
      await deleteSearchCacheDatabase();
      // Other localStorage caches also hold decrypted plaintext / PII
      // (translation, scheduled messages, recent searches/forwards/gifs/
      // stickers, nav paths) — wipe them on server-forced logout too.
      clearPlaintextCaches(mx?.getUserId() ?? undefined);
      // Remove only the session credential keys — NOT settings, drafts, and
      // other preferences (N98). The SDK's IndexedDB stores are cleared above;
      // window.localStorage.clear() is reserved for the explicit reset path.
      removeFallbackSession();
      window.location.reload();
    };

    mx?.on(HttpApiEvent.SessionLoggedOut, handleLogout);
    return () => {
      mx?.removeListener(HttpApiEvent.SessionLoggedOut, handleLogout);
    };
  }, [mx]);
};

type ClientRootProps = {
  children: ReactNode;
};
export function ClientRoot({ children }: ClientRootProps) {
  const [loading, setLoading] = useState(true);
  const [syncError, setSyncError] = useState(false);
  // Tracks whether the initial sync has ever reached PREPARED. After that,
  // transient sync errors are handled by <SyncStatus>'s reconnection banner,
  // so we must NOT pop the blocking error splash for them.
  const hasPreparedRef = useRef(false);
  const { baseUrl, userId } = getFallbackSession() ?? {};

  const [loadState, loadMatrix] = useAsyncCallback<MatrixClient, Error, []>(
    useCallback(() => {
      const session = getFallbackSession();
      if (!session) {
        throw new Error('No session Found!');
      }
      return initClient(session);
    }, []),
  );
  const mx = loadState.status === AsyncStatus.Success ? loadState.data : undefined;
  const [startState, startMatrix] = useAsyncCallback<void, Error, [MatrixClient]>(
    useCallback((m) => startClient(m), []),
  );

  useLogoutListener(mx);
  // Cross-tab session sync: another tab logging out / in (access token changed
  // in localStorage) reloads this tab so it never runs with stale credentials.
  useSessionSync();

  useEffect(() => {
    if (loadState.status === AsyncStatus.Idle) {
      loadMatrix();
    }
  }, [loadState, loadMatrix]);

  useEffect(() => {
    if (mx && !mx.clientRunning) {
      startMatrix(mx);
    }
  }, [mx, startMatrix]);

  useSyncState(
    mx,
    useCallback((state) => {
      if (state === 'PREPARED') {
        hasPreparedRef.current = true;
        setSyncError(false);
        setLoading(false);
      } else if (state === 'ERROR' || state === 'STOPPED') {
        // Only surface the blocking error splash when the INITIAL sync fails
        // (offline at startup, homeserver unreachable, non-retryable /sync
        // error). After the first PREPARED, <SyncStatus> owns reconnection UX.
        if (!hasPreparedRef.current) setSyncError(true);
      }
    }, []),
  );

  return (
    <AutoDiscovery userId={userId!} baseUrl={baseUrl!}>
      <SpecVersions baseUrl={baseUrl!}>
        {mx && !syncError && <SyncStatus mx={mx} />}
        {loading && <ClientRootOptions mx={mx} />}
        {(loadState.status === AsyncStatus.Error ||
          startState.status === AsyncStatus.Error ||
          syncError) && (
          <SplashScreen>
            <Box
              direction="Column"
              grow="Yes"
              alignItems="Center"
              justifyContent="Center"
              gap="400"
            >
              <Dialog>
                <Box direction="Column" gap="400" style={{ padding: config.space.S400 }}>
                  {loadState.status === AsyncStatus.Error && (
                    <>
                      {loadState.error.message === IDB_VERSION_CONFLICT ? (
                        <>
                          <Text>
                            Local data is from a newer app version and cannot be read. Clear local
                            data to continue (you will need to log in again).
                          </Text>
                          <Button variant="Critical" onClick={clearLoginData}>
                            <Text as="span" size="B400">
                              Clear local data and reload
                            </Text>
                          </Button>
                        </>
                      ) : (
                        <Text>{`Failed to load. ${loadState.error.message}`}</Text>
                      )}
                    </>
                  )}
                  {startState.status === AsyncStatus.Error && (
                    <Text>{`Failed to start. ${startState.error.message}`}</Text>
                  )}
                  {syncError &&
                    loadState.status !== AsyncStatus.Error &&
                    startState.status !== AsyncStatus.Error && (
                      <Text>
                        Failed to sync with your homeserver. Check your connection and try again.
                      </Text>
                    )}
                  {('error' in loadState ? (loadState as any).error?.message : undefined) !==
                    IDB_VERSION_CONFLICT && (
                    <Button variant="Critical" onClick={mx ? () => startMatrix(mx) : loadMatrix}>
                      <Text as="span" size="B400">
                        Retry
                      </Text>
                    </Button>
                  )}
                </Box>
              </Dialog>
            </Box>
          </SplashScreen>
        )}
        {loading || !mx ? (
          <ClientRootLoading />
        ) : (
          <MatrixClientProvider value={mx}>
            <ServerConfigsLoader>
              {(serverConfigs) => (
                <CapabilitiesProvider value={serverConfigs.capabilities ?? {}}>
                  <MediaConfigProvider value={serverConfigs.mediaConfig ?? {}}>
                    <AuthMetadataProvider value={serverConfigs.authMetadata}>
                      {children}
                    </AuthMetadataProvider>
                  </MediaConfigProvider>
                </CapabilitiesProvider>
              )}
            </ServerConfigsLoader>
          </MatrixClientProvider>
        )}
      </SpecVersions>
    </AutoDiscovery>
  );
}
