import {
  type DeviceRecord,
  getOwnProfile,
  signOut as supabaseSignOut,
  type Profile,
  touchDeviceLastSeen,
  updateOwnProfile,
} from '@chat-app/shared/auth';
import { changeLocale, isSupportedLocale } from '@chat-app/shared/i18n';
import type { Session } from '@supabase/supabase-js';
import {
  createContext,
  type ReactNode,
  useCallback,
  useContext,
  useEffect,
  useMemo,
  useRef,
  useState,
} from 'react';
import { useTranslation } from 'react-i18next';

import { findExistingDevice } from '../lib/device';
import { PRESENCE_HEARTBEAT_MS } from '../lib/presence';
import { setSecretStoreUser } from '../lib/secretStore';
import { supabase } from '../lib/supabase';
import { registerWebPush } from '../lib/webPush';

interface AuthContextValue {
  session: Session | null;
  profile: Profile | null;
  device: DeviceRecord | null;
  // null while we're still resolving the very first auth state.
  ready: boolean;
  // null until a device lookup has finished for the current session.
  deviceLookupDone: boolean;
  refreshProfile: () => Promise<void>;
  refreshDevice: () => Promise<void>;
  setDevice: (device: DeviceRecord | null) => void;
  signOut: () => Promise<void>;
}

const AuthContext = createContext<AuthContextValue | null>(null);

export function AuthProvider({ children }: { children: ReactNode }) {
  const { i18n } = useTranslation();
  const [session, setSession] = useState<Session | null>(null);
  const [ready, setReady] = useState(false);
  const [profile, setProfile] = useState<Profile | null>(null);
  const [device, setDevice] = useState<DeviceRecord | null>(null);
  const [deviceLookupDone, setDeviceLookupDone] = useState(false);
  const autoOnlineUserRef = useRef<string | null>(null);

  // Initial session + auth subscription. We verify the cached JWT against the
  // server (via getUser) once on mount. Only purge the session on an
  // unambiguous 401/403 — a network failure (Supabase stack offline) must not
  // log the user out, otherwise every local `supabase stop` wipes their session.
  useEffect(() => {
    let cancelled = false;
    (async () => {
      const { data: sessionRes } = await supabase.auth.getSession();
      if (cancelled) return;
      // Flip `ready` immediately on cached session read so the UI unblocks even
      // if the network is slow/down. Validate the token in the background and
      // only wipe on an unambiguous 401/403 — a stalled getUser (Tauri WebView
      // with no network, server unreachable) must not keep the app on the
      // loading spinner forever.
      setSession(sessionRes.session ?? null);
      setReady(true);

      if (sessionRes.session) {
        supabase.auth
          .getUser()
          .then(({ error }) => {
            if (cancelled || !error) return;
            const status = (error as { status?: number }).status;
            if (status === 401 || status === 403) {
              void supabase.auth.signOut({ scope: 'local' }).catch(() => {
                /* ignore */
              });
              setSession(null);
            } else {
              // Network / server unreachable — keep cached session.
              console.warn('auth.getUser failed, keeping cached session:', error);
            }
          })
          .catch((err: unknown) => {
            console.warn('auth.getUser rejected, keeping cached session:', err);
          });
      }
    })();
    const { data: sub } = supabase.auth.onAuthStateChange((_event, s) => {
      setSession(s);
      setReady(true);
      void setSecretStoreUser(s?.user.id ?? null);
    });
    return () => {
      cancelled = true;
      sub.subscription.unsubscribe();
    };
  }, []);

  const refreshProfile = useCallback(async () => {
    if (!session) {
      setProfile(null);
      return;
    }
    const p = await getOwnProfile(supabase);
    setProfile(p);
    if (p && p.locale !== i18n.resolvedLanguage && isSupportedLocale(p.locale)) {
      void changeLocale(p.locale);
    }
  }, [session, i18n.resolvedLanguage]);

  const refreshDevice = useCallback(async () => {
    if (!session) {
      setDevice(null);
      setDeviceLookupDone(false);
      return;
    }
    setDeviceLookupDone(false);
    const found = await findExistingDevice(session.user.id);
    setDevice(found);
    setDeviceLookupDone(true);
  }, [session]);

  // Re-pull profile + device whenever session flips.
  useEffect(() => {
    if (!session) {
      setProfile(null);
      setDevice(null);
      setDeviceLookupDone(false);
      return;
    }
    void refreshProfile().catch((err: unknown) => {
      console.error('refreshProfile failed', err);
    });
    void refreshDevice().catch((err: unknown) => {
      console.error('refreshDevice failed', err);
      setDeviceLookupDone(true);
    });
  }, [session, refreshProfile, refreshDevice]);

  // Best-effort web-push registration once we know the device id. No-op on
  // Tauri (uses native notifications) or when VITE_VAPID_PUBLIC_KEY is unset.
  useEffect(() => {
    if (!device?.id) return;
    void registerWebPush(device.id);
  }, [device?.id]);

  // Auto online/offline transition.
  //
  // - On mount with a session whose last persisted state is `offline`, flip
  //   to `online`. We never override an explicit `idle`, `dnd`, or
  //   `invisible` choice — those are user intent.
  // - On `pagehide` / `beforeunload`, fire a best-effort update to
  //   `offline`. Browsers don't guarantee delivery during unload, but the
  //   request usually slips through; the next page load corrects state if it
  //   didn't.
  useEffect(() => {
    if (!session) {
      autoOnlineUserRef.current = null;
      return;
    }
    if (!profile) return;
    const shouldApplyInitialAutoOnline = autoOnlineUserRef.current !== session.user.id;
    autoOnlineUserRef.current = session.user.id;
    if (profile.presenceState === 'offline' && shouldApplyInitialAutoOnline) {
      void updateOwnProfile(supabase, { presenceState: 'online' })
        .then(() => refreshProfile())
        .catch((err: unknown) => {
          console.warn('auto online flip failed', err);
        });
    }
    const onLeave = () => {
      // Skip if user explicitly chose a non-online state — they probably
      // want to look unavailable on next reconnect too.
      if (profile.presenceState !== 'online' && profile.presenceState !== 'offline') {
        return;
      }
      void updateOwnProfile(supabase, { presenceState: 'offline' }).catch(() => {});
    };
    window.addEventListener('beforeunload', onLeave);
    window.addEventListener('pagehide', onLeave);
    return () => {
      window.removeEventListener('beforeunload', onLeave);
      window.removeEventListener('pagehide', onLeave);
    };
  }, [session, profile, refreshProfile]);

  useEffect(() => {
    if (!session || !device?.id) return;

    const touch = () => {
      void touchDeviceLastSeen(supabase, device.id).catch((err: unknown) => {
        console.warn('presence heartbeat failed', err);
      });
    };
    const touchWhenVisible = () => {
      if (document.visibilityState === 'visible') touch();
    };

    touch();
    const heartbeat = window.setInterval(touch, PRESENCE_HEARTBEAT_MS);
    window.addEventListener('focus', touch);
    window.addEventListener('online', touch);
    document.addEventListener('visibilitychange', touchWhenVisible);

    return () => {
      window.clearInterval(heartbeat);
      window.removeEventListener('focus', touch);
      window.removeEventListener('online', touch);
      document.removeEventListener('visibilitychange', touchWhenVisible);
    };
  }, [session, device?.id]);

  const signOut = useCallback(async () => {
    await updateOwnProfile(supabase, { presenceState: 'offline' }).catch((err: unknown) => {
      console.warn('offline update before sign-out failed', err);
    });
    await supabaseSignOut(supabase);
  }, []);

  const value = useMemo<AuthContextValue>(
    () => ({
      session,
      profile,
      device,
      ready,
      deviceLookupDone,
      refreshProfile,
      refreshDevice,
      setDevice,
      signOut,
    }),
    [session, profile, device, ready, deviceLookupDone, refreshProfile, refreshDevice, signOut],
  );

  return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}

export function useAuth(): AuthContextValue {
  const ctx = useContext(AuthContext);
  if (!ctx) throw new Error('useAuth must be used inside <AuthProvider>');
  return ctx;
}
