./knowledge-base/frontend/src/auth/AuthContext.tsx

/**
 * 認証コンテキスト(Okta 実装)
 *
 * OktaAuth の認証状態をアプリ全体に提供する。
 * AuthContextValue のインターフェースは変えていないため、
 * useAuth() を使っている各ページの変更は不要。
 */

import { createContext, useContext, useEffect, useState } from "react";
import type { ReactNode } from "react";
import type { AuthState } from "@okta/okta-auth-js";
import { oktaAuth } from "./oktaConfig";

export interface AuthUser {
  /** Okta の sub クレーム */
  id: string;
  /** Okta の name クレーム */
  name: string;
  /** Okta の email クレーム */
  email: string;
}

export interface AuthContextValue {
  /** 認証済みかどうか */
  isAuthenticated: boolean;
  /** 認証状態のロード中かどうか(初回チェック完了前は true) */
  isLoading: boolean;
  /** ログイン中ユーザー情報(未認証時は null) */
  user: AuthUser | null;
  /** Okta ログインページへリダイレクト */
  login: () => void;
  /** Okta セッションを破棄してログアウト */
  logout: () => void;
  /** API 呼び出し用 Access Token を取得 */
  getAccessToken: () => string | undefined;
}

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

/**
 * 認証トークンを Cookie に保存する
 * Lambda@Edge が /era-assist/* の認証確認に使用
 * @param token Okta Access Token
 */
function saveTokenToCookie(token: string | undefined): void {
  if (!token) {
    // ログアウト時は Cookie を削除
    document.cookie =
      "okta_access_token=; path=/; max-age=0; SameSite=Lax; Secure";
    return;
  }
  // JWT の exp クレームから有効期限を取得
  try {
    const payload = JSON.parse(atob(token.split(".")[1]));
    const maxAge = payload.exp - Math.floor(Date.now() / 1000);
    if (maxAge > 0) {
      document.cookie = `okta_access_token=${token}; path=/; max-age=${maxAge}; SameSite=Lax; Secure`;
    }
  } catch {
    // デコード失敗時はデフォルト 1時間
    document.cookie = `okta_access_token=${token}; path=/; max-age=3600; SameSite=Lax; Secure`;
  }
}

/** 認証コンテキストプロバイダー */
export function AuthProvider({ children }: { children: ReactNode }) {
  const [authState, setAuthState] = useState<AuthState | null>(
    oktaAuth.authStateManager.getAuthState(),
  );

  useEffect(() => {
    // 認証状態が変化したら再描画
    const handler = (newState: AuthState) => setAuthState(newState);
    oktaAuth.authStateManager.subscribe(handler);

    // コールバック処理中でなければサービス開始(トークン自動更新など)
    if (!oktaAuth.isLoginRedirect()) {
      oktaAuth.start();
    }

    return () => {
      oktaAuth.authStateManager.unsubscribe(handler);
    };
  }, []);

  // authState === null の間はロード中
  const isLoading = authState === null;
  const isAuthenticated = authState?.isAuthenticated ?? false;

  const user: AuthUser | null = authState?.idToken?.claims
    ? {
        id: String(authState.idToken.claims.sub ?? ""),
        name: String(authState.idToken.claims.name ?? ""),
        email: String(authState.idToken.claims.email ?? ""),
      }
    : null;

  // 認証状態変化時にトークンを Cookie に同期する
  useEffect(() => {
    if (isAuthenticated) {
      saveTokenToCookie(authState?.accessToken?.accessToken);
    } else if (!isLoading) {
      // ログアウトまたは未認証: Cookie が存在する場合は削除
      saveTokenToCookie(undefined);
    }
  }, [isAuthenticated, isLoading, authState?.accessToken?.accessToken]);

  const login = () => {
    // 現在の URL(?redirect_to= 含む)を originalUri として保存する
    // 認証後に LoginCallbackPage がここから redirect_to を取り出す
    oktaAuth.signInWithRedirect({
      originalUri:
        window.location.pathname +
        window.location.search +
        window.location.hash,
    });
  };

  const logout = () => {
    oktaAuth.signOut();
  };

  const getAccessToken = (): string | undefined => {
    return authState?.accessToken?.accessToken;
  };

  return (
    <AuthContext.Provider
      value={{
        isAuthenticated,
        isLoading,
        user,
        login,
        logout,
        getAccessToken,
      }}
    >
      {children}
    </AuthContext.Provider>
  );
}

/** 認証コンテキストを取得するカスタムフック */
export function useAuth(): AuthContextValue {
  const ctx = useContext(AuthContext);
  if (!ctx) throw new Error("useAuth must be used within AuthProvider");
  return ctx;
}