import { Box, Button, config, Dialog, Text } from 'folds';
import { HttpApiEvent, HttpApiEventHandlerMap, MatrixClient, SyncState } from 'matrix-js-sdk';
import React, { ReactNode, useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { clearLocalSessionAndReload, initClient, startClient } from '../../../client/initMatrix';
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 { useSyncPresenceGate } from '../../hooks/useSyncPresenceGate';
import { SyncIndicator } from './SyncIndicator';
import { AuthMetadataProvider } from '../../hooks/useAuthMetadata';
import { getFallbackSession } from '../../state/sessions';
import { AutoDiscovery } from './AutoDiscovery';
import { AuthSplashScreen } from '../auth/AuthSplashScreen';
import { clearSessionBridge, writeSessionBridge } from '../../utils/sessionBridge';
import { polling } from '../../plugins/polling';
import { proxy } from '../../plugins/proxy';
import { useProxy } from '../../hooks/useProxy';

function ClientRootLoading() {
  return <AuthSplashScreen />;
}

const useLogoutListener = (mx?: MatrixClient) => {
  useEffect(() => {
    const handleLogout: HttpApiEventHandlerMap[HttpApiEvent.SessionLoggedOut] = async () => {
      // Wipe the native session bridge before the reload — otherwise the dead
      // access_token lingers in shared_prefs and CallDeclineReceiver spends
      // the next login cycle posting 401s until writeSessionBridge overwrites.
      await clearSessionBridge();
      // Wipe the Tink-encrypted proxy creds + clear the override + tear down
      // the relay on a server-driven logout too (proxy_support.md §15, locked).
      await proxy.clearConfig();
      // Same applies to the WorkManager polling fallback: it has its own
      // SharedPreferences-stored access_token that the React lifecycle
      // cleanup wouldn't get a chance to clear before window.location.reload.
      await polling.cancel();
      await polling.clearSession();
      mx?.stopClient();
      await mx?.clearStores();
      window.localStorage.clear();
      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 { t } = useTranslation();
  const [loading, setLoading] = useState(true);
  // Latches when the SDK reports SyncState.Error before the first PREPARED
  // arrives — at that point matrix-js-sdk has already burned through its 3-try
  // keep-alive backoff (~15-30s), so it's a definitive "I gave up" signal, not
  // a transient blip. Cleared on Syncing in case the network recovers and the
  // SDK pulls itself out before PREPARED.
  const [syncErrored, setSyncErrored] = useState(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);
  // The proxy override + status atom (Android-only; no-op on web). Used by the
  // post-login narrow effect below to scope a pre-login route-all override AND
  // to run the reachability probe so the status stops reading "Connecting…"
  // once we have a homeserver to measure against (F1/F18).
  const { setEnabled: setProxyEnabled, probe: proxyProbe } = useProxy();
  // Mark the user as Online during foreground and Unavailable while the
  // tab is hidden / app is backgrounded, so background long-poll /sync
  // calls stop fraudulently bumping last_active_ago on the homeserver.
  // Without this every other Vojo client renders "был в сети только что"
  // for a peer whose phone has been sleeping for hours — see
  // useSyncPresenceGate.ts for the full rationale.
  useSyncPresenceGate(mx);

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

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

  // Mirror {accessToken, baseUrl, userId} into native SharedPreferences so
  // CallDeclineReceiver can send m.call.decline without booting the WebView.
  // No-op on web.
  useEffect(() => {
    if (!mx) return;
    writeSessionBridge(mx);
  }, [mx]);

  // If an Android proxy was configured pre-login (route-all during discovery),
  // narrow it to the now-resolved homeserver host so external URL-preview /
  // widget origins stop riding the user's proxy (proxy_android_impl.md §2).
  // Idempotent + no-op when the proxy is off or on web.
  useEffect(() => {
    if (!mx) return;
    (async () => {
      try {
        const state = await proxy.getState();
        if (!state.enabled) return;
        const host = new URL(mx.baseUrl).hostname;
        if (state.scopeHost === host) {
          // Already scoped to this exact host — the common logged-in boot path,
          // where applyAtBoot ran with the same host. Don't re-apply: apply()
          // tears down and restarts the relay, dropping the /sync tunnel that
          // just started and rotating the relay token on every boot (F6). But
          // DO probe — applyAtBoot was native-only, so the status is still
          // 'connecting'; this flips it to ok/error + measures the ping (F1).
          await proxyProbe(mx.baseUrl);
          return;
        }
        // Coming from a pre-login route-all override (scopeHost null or another
        // host) — narrow it to the resolved homeserver host so external
        // URL-preview / widget origins stop riding the user's proxy. setEnabled
        // (the hook) also probes once applied, so the status becomes honest the
        // moment a homeserver exists to measure against (F18).
        await setProxyEnabled(true, mx.baseUrl);
      } catch {
        // ignore — the boot-time apply already covers the logged-in path.
      }
    })();
  }, [mx, setProxyEnabled, proxyProbe]);

  // When the OS reports the network is back, prod the sync loop instead of
  // waiting for matrix-js-sdk's internal keep-alive jitter (5–10s backoff
  // per `sync.js`). Only acts when the SDK is genuinely paused on a failed
  // connection — Error or Reconnecting — so a healthy Syncing client just
  // ignores the event. `retryImmediately` is itself idempotent, so spurious
  // duplicate `online` events from flaky NICs are harmless.
  useEffect(() => {
    if (!mx) return undefined;
    const onOnline = () => {
      const state = mx.getSyncState();
      if (state === SyncState.Error || state === SyncState.Reconnecting) {
        mx.retryImmediately();
      }
    };
    window.addEventListener('online', onOnline);
    return () => window.removeEventListener('online', onOnline);
  }, [mx]);

  useSyncState(
    mx,
    useCallback((state) => {
      if (state === SyncState.Prepared) {
        setLoading(false);
        setSyncErrored(false);
      } else if (state === SyncState.Error) {
        setSyncErrored(true);
      } else if (state === SyncState.Syncing) {
        setSyncErrored(false);
      }
    }, [])
  );

  // Sync-error case: SDK is wired up correctly, just stuck — poke the next
  // /sync. Init/start failures need to re-run the failed step from scratch.
  let onRetry: () => void;
  if (loading && syncErrored && mx) {
    onRetry = () => mx.retryImmediately();
  } else if (mx) {
    onRetry = () => startMatrix(mx);
  } else {
    onRetry = loadMatrix;
  }

  // When `getFallbackSession()` returns `undefined` (no stored session),
  // `loadMatrix()` throws and the error branch below renders inside
  // `AuthSplashScreen`. The empty-string defaults reach AutoDiscovery /
  // SpecVersions but neither component does anything load-bearing with them
  // in the error path — AutoDiscovery falls back to an empty `m.homeserver`
  // record, SpecVersions probes the current origin (harmless).
  return (
    <AutoDiscovery userId={userId ?? ''} baseUrl={baseUrl ?? ''}>
      <SpecVersions baseUrl={baseUrl ?? ''}>
        {mx && <SyncIndicator mx={mx} />}
        {(loadState.status === AsyncStatus.Error ||
          startState.status === AsyncStatus.Error ||
          (loading && syncErrored)) && (
          <AuthSplashScreen>
            <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 && (
                    <Text>
                      {t('Boot.client_load_failed', { message: loadState.error.message })}
                    </Text>
                  )}
                  {startState.status === AsyncStatus.Error && (
                    <Text>
                      {t('Boot.client_start_failed', { message: startState.error.message })}
                    </Text>
                  )}
                  {loading &&
                    syncErrored &&
                    loadState.status !== AsyncStatus.Error &&
                    startState.status !== AsyncStatus.Error && <Text>{t('Boot.sync_failed')}</Text>}
                  <Button variant="Critical" onClick={onRetry}>
                    <Text as="span" size="B400">
                      {t('Boot.retry')}
                    </Text>
                  </Button>
                  {/* Init/start failures (corrupt IndexedDB, dead crypto store)
                      and pre-PREPARED sync errors share the same recovery —
                      wipe local session state and reload. Network calls are
                      skipped: in every case here the network is the suspect or
                      irrelevant, and the homeserver will time out the session
                      naturally. */}
                  <Button variant="Secondary" fill="Soft" onClick={clearLocalSessionAndReload}>
                    <Text as="span" size="B400">
                      {t('Boot.logout')}
                    </Text>
                  </Button>
                </Box>
              </Dialog>
            </Box>
          </AuthSplashScreen>
        )}
        {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>
  );
}
