'use client';

import { createContext, useCallback, useContext, useEffect, useRef, useState } from 'react';

type AdminUser = { id: number; name: string; email: string; role: string };

type AuthState = {
  user: AdminUser | null;
  accessToken: string | null;
  appSecretProof: string | null;
  permissions: string[];
  loading: boolean;
  /** Signed in but away too long / idle: shows the "enter your code" lock screen instead of the password login. */
  locked: boolean;
};

type AuthContextValue = AuthState & {
  login: (email: string, password: string) => Promise<{ status: string; preAuthToken?: string; userId?: number }>;
  verifyTwoFactor: (email: string, token: string) => Promise<void>;
  logout: () => Promise<void>;
  /** Unlock a locked session with the 6-digit authenticator code only (no password). */
  unlock: (code: string) => Promise<void>;
  lockNow: () => Promise<void>;
  hasPermission: (key: string | null) => boolean;
};

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

// Idle lock: no mouse / keyboard / touch for this long → lock screen (code only).
const IDLE_LOCK_MINUTES = Number(process.env.NEXT_PUBLIC_IDLE_LOCK_MINUTES || 15);
// Access tokens live 15 min; renew at 12 so a request never lands on an expired one.
const RENEW_AFTER_MS = 12 * 60 * 1000;

const EMPTY: AuthState = { user: null, accessToken: null, appSecretProof: null, permissions: [], loading: false, locked: false };

// Tokens live in React state only, never localStorage/sessionStorage: an XSS
// payload can't persist them past the page. What survives a reload, a sleeping
// laptop or a powered-off PC is the httpOnly refresh cookie (unreadable by JS),
// which lets the person back in with just their authenticator code.
export function AuthProvider({ children }: { children: React.ReactNode }) {
  const [state, setState] = useState<AuthState>({ ...EMPTY, loading: true });
  const stateRef = useRef(state);
  stateRef.current = state;
  const issuedAt = useRef(0);
  const lastActivity = useRef(Date.now());
  const busy = useRef(false);

  const applySession = useCallback((data: { user?: AdminUser; accessToken: string; appSecretProof: string; permissions?: string[] }) => {
    issuedAt.current = Date.now();
    lastActivity.current = Date.now();
    setState((s) => ({
      user: data.user ?? s.user, accessToken: data.accessToken, appSecretProof: data.appSecretProof,
      permissions: data.permissions ?? s.permissions, loading: false, locked: false,
    }));
  }, []);

  const setLocked = useCallback((info?: { name: string; email: string }) => {
    setState((s) => ({
      ...s, loading: false, locked: true, accessToken: null, appSecretProof: null,
      user: s.user ?? (info ? { id: 0, name: info.name, email: info.email, role: '' } : null),
    }));
  }, []);

  // Silent refresh. 200 → new tokens · 423 → locked (needs code) · anything else → signed out.
  const refreshOnce = useCallback(async () => {
    if (busy.current) return;
    busy.current = true;
    try {
      const res = await fetch('/api/auth/refresh', { method: 'POST' });
      if (res.ok) applySession(await res.json());
      else if (res.status === 423) { const d = await res.json().catch(() => ({})); setLocked(d.user); }
      else setState({ ...EMPTY });
    } catch {
      // Network blip (e.g. PC just woke up and Wi-Fi isn't back): keep whatever we have and retry on the next tick.
    } finally { busy.current = false; }
  }, [applySession, setLocked]);

  const lockNow = useCallback(async () => {
    if (!stateRef.current.user || stateRef.current.locked) return;
    setLocked();
    await fetch('/api/auth/lock', { method: 'POST' }).catch(() => null);
  }, [setLocked]);

  // On mount: try to continue the existing session.
  useEffect(() => { refreshOnce(); }, [refreshOnce]);

  // One ticker drives both renewal and the idle lock. A ticker (not a 12-minute
  // setInterval) survives laptop sleep: on wake the next tick sees how much time
  // really passed and acts on it.
  useEffect(() => {
    const onActivity = () => { lastActivity.current = Date.now(); };
    const events = ['mousemove', 'mousedown', 'keydown', 'touchstart', 'scroll', 'wheel'];
    events.forEach((e) => window.addEventListener(e, onActivity, { passive: true }));

    const tick = () => {
      const s = stateRef.current;
      if (!s.user || s.locked || s.loading) return;
      if (Date.now() - lastActivity.current > IDLE_LOCK_MINUTES * 60 * 1000) { lockNow(); return; }
      if (Date.now() - issuedAt.current > RENEW_AFTER_MS) refreshOnce();
    };
    const timer = setInterval(tick, 30_000);
    const onVisible = () => { if (document.visibilityState === 'visible') tick(); };
    document.addEventListener('visibilitychange', onVisible);
    window.addEventListener('focus', tick);
    return () => {
      events.forEach((e) => window.removeEventListener(e, onActivity));
      clearInterval(timer);
      document.removeEventListener('visibilitychange', onVisible);
      window.removeEventListener('focus', tick);
    };
  }, [lockNow, refreshOnce]);

  const login = useCallback(async (email: string, password: string) => {
    const res = await fetch('/api/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email, password }) });
    const data = await res.json();
    if (!res.ok) throw new Error(data.error || 'LOGIN_FAILED');
    return data;
  }, []);

  const verifyTwoFactor = useCallback(async (email: string, token: string) => {
    const res = await fetch('/api/auth/2fa', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email, token }) });
    const data = await res.json();
    if (!res.ok) throw new Error(data.error || 'TWO_FACTOR_FAILED');
    applySession(data);
  }, [applySession]);

  const unlock = useCallback(async (code: string) => {
    const res = await fetch('/api/auth/resume', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ token: code }) });
    const data = await res.json().catch(() => ({}));
    if (!res.ok) {
      // Session gone entirely (cookie expired / revoked): fall back to the normal login.
      if (res.status === 403) { setState({ ...EMPTY }); }
      throw new Error(data.error || 'UNLOCK_FAILED');
    }
    applySession(data);
  }, [applySession]);

  const logout = useCallback(async () => {
    await fetch('/api/auth/logout', { method: 'POST' }).catch(() => null);
    setState({ ...EMPTY });
    window.location.href = '/login';
  }, []);

  // `null` permission means "no permission required" (Dashboard, Settings).
  const hasPermission = useCallback((key: string | null) => {
    if (key === null) return true;
    return state.permissions.includes(key);
  }, [state.permissions]);

  return (
    <AuthContext.Provider value={{ ...state, login, verifyTwoFactor, logout, unlock, lockNow, hasPermission }}>
      {children}
    </AuthContext.Provider>
  );
}

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