/**
 * Ephemeral Token Manager
 *
 * NS 移植版: OpenAI Realtime API のエフェメラルトークンを
 * `POST {restUrl}realtime-token` から取得する。
 *
 * pocxr 本番はクライアントが session 設定 (instructions/tools/turn_detection/
 * transcription/audio format 等) を組み立ててトークン発行エンドポイントに
 * 送っていたが、NS はサーバー側 (`realtime-token` エンドポイント) がテナント設定から
 * instructions/voice/turn_detection/transcription を全て確定させてエフェメラル
 * トークンを発行するため、クライアントは **body 無しの POST** のみを行う
 * (計画書 §1.2.2 / §3.2(2))。レスポンス形状は
 * `{ client_secret: { value, expires_at }, model, voice }`。
 */

import { createLogger } from "./debug-logger.js";
import { nsFetch } from "../ns/ns-rest.js";

// デバッグロガー
const logger = createLogger('EphemeralTokenManager');

// トークンの有効期限前にリフレッシュするためのバッファ（秒）
const EXPIRY_BUFFER_SECONDS = 30;

export class EphemeralTokenManager {
  constructor() {
    this.cachedToken = null;
    this.cachedExpiresAt = null;
    this.cachedModel = null;
    this.cachedVoice = null;
    this.fetchPromise = null;
    this.logCallback = null;
    // 直近の fetchToken() が非2xxで失敗した場合の HTTP status。成功時は null に戻す。
    // 呼び出し元 (openai-realtime-connector.js) が err.httpStatus を数値プロパティとして
    // 付与するために参照する — ローカライズ済みメッセージ文字列のパースは行わない契約。
    this.lastErrorStatus = null;
  }

  /**
   * ログコールバックを設定
   * @param {Function} callback - ログ出力用コールバック関数
   */
  setLogCallback(callback) {
    this.logCallback = callback;
  }

  /**
   * ログを出力
   * @param {string} message - ログメッセージ
   */
  log(message) {
    logger.log(message);
    if (this.logCallback) {
      this.logCallback(`[EphemeralTokenManager] ${message}`);
    }
  }

  /**
   * トークンが有効かどうかをチェック
   * NS では instructions によるキャッシュ無効化は行わない (サーバー確定値のため
   * クライアント側で変化を検知する必要が無い) — 有効期限のみを見る。
   * @returns {boolean} トークンが有効な場合true
   */
  isTokenValid() {
    if (!this.cachedToken || !this.cachedExpiresAt) {
      return false;
    }

    const now = Math.floor(Date.now() / 1000);
    const expiresAt = this.cachedExpiresAt - EXPIRY_BUFFER_SECONDS;

    if (now >= expiresAt) {
      this.log("The token has expired");
      return false;
    }

    const remainingSeconds = expiresAt - now;
    this.log(`The cached token is still valid (${remainingSeconds}s left)`);
    return true;
  }

  /**
   * サーバーからトークンを取得 (body 無し POST)
   * @param {Object} [_sessionConfig] - 呼び出し元互換のため受け取るが使用しない
   *   (NS はサーバー側で session 設定を確定するため)
   * @returns {Promise<string|null>} トークン、または取得失敗時はnull
   */
  async fetchToken(_sessionConfig) {
    try {
      this.log("Fetching the token...");
      const response = await nsFetch("realtime-token", {
        method: "POST",
      });

      if (!response.ok) {
        this.log(`Token fetch failed: HTTP ${response.status}`);
        this.lastErrorStatus = response.status;
        return null;
      }

      const data = await response.json();

      if (!data.client_secret) {
        this.log("client_secret not found");
        // レスポンス自体は2xxなのでHTTP statusとしては伝えるものが無い
        this.lastErrorStatus = null;
        return null;
      }

      // キャッシュを更新
      this.cachedToken = data.client_secret.value;
      this.cachedExpiresAt = data.client_secret.expires_at;
      this.cachedModel = data.model || null;
      this.cachedVoice = data.voice || null;
      this.lastErrorStatus = null;

      this.log(`Token acquired (expires: ${new Date(this.cachedExpiresAt * 1000).toLocaleString()})`);
      return this.cachedToken;
    } catch (error) {
      this.log(`Token fetch error: ${error.message}`);
      // fetch自体が例外（ネットワーク断等）の場合はHTTP statusが存在しない
      this.lastErrorStatus = null;
      return null;
    }
  }

  /**
   * 直近の fetchToken() が非2xxで失敗した場合の HTTP status を返す。
   * ネットワーク例外時や未失敗時は null。
   * @returns {number|null}
   */
  getLastErrorStatus() {
    return this.lastErrorStatus;
  }

  /**
   * ページ読み込み時にトークンを事前取得
   * @param {Object} [sessionConfig] - 呼び出し元互換のため受け取るが使用しない
   * @returns {Promise<void>}
   */
  async prefetch(sessionConfig) {
    this.log("Prefetching the token...");

    // 既に取得中の場合は、そのPromiseを返す
    if (this.fetchPromise) {
      this.log("A token fetch is already in flight; waiting for it to finish...");
      return this.fetchPromise;
    }

    this.fetchPromise = this.fetchToken(sessionConfig);

    try {
      await this.fetchPromise;
    } finally {
      this.fetchPromise = null;
    }
  }

  /**
   * トークンを取得（キャッシュがあれば使用、なければ取得）
   * @param {Object} [sessionConfig] - 呼び出し元互換のため受け取るが使用しない
   * @returns {Promise<string|null>} トークン、または取得失敗時はnull
   */
  async getToken(sessionConfig) {
    // キャッシュが有効な場合はそれを返す
    if (this.isTokenValid()) {
      this.log("Using the cached token");
      return this.cachedToken;
    }

    // 既に取得中の場合は、そのPromiseを待つ
    if (this.fetchPromise) {
      this.log("A token fetch is in flight; waiting for it to finish...");
      await this.fetchPromise;

      // 取得完了後、キャッシュが有効か再チェック
      if (this.isTokenValid()) {
        return this.cachedToken;
      }
    }

    // 新しくトークンを取得
    return this.fetchToken(sessionConfig);
  }

  /**
   * 最後にトークン取得時に返された model 名を取得する。
   * `POST https://api.openai.com/v1/realtime/calls?model=` に使用する (§3.2)。
   * @returns {string|null}
   */
  getLastModel() {
    return this.cachedModel;
  }

  /**
   * 最後にトークン取得時に返された voice 名を取得する。
   * @returns {string|null}
   */
  getLastVoice() {
    return this.cachedVoice;
  }

  /**
   * 後方互換のためのファサード。NS はサーバー側で session 設定を確定するため
   * output_modalities/tools/audio をクライアントが再構築する必要が無い —
   * 常に `null` を返し、呼び出し元 (`connect()`) の
   * `setSessionConfig()` 呼び出しをスキップさせる。
   * @returns {null}
   */
  getLastSessionConfig() {
    return null;
  }

  /**
   * キャッシュをクリア
   */
  clearCache() {
    this.cachedToken = null;
    this.cachedExpiresAt = null;
    this.cachedModel = null;
    this.cachedVoice = null;
    this.fetchPromise = null;
    this.log("Cache cleared");
  }

  /**
   * キャッシュの状態を取得
   * @returns {Object} キャッシュの状態
   */
  getCacheStatus() {
    const now = Math.floor(Date.now() / 1000);
    const remainingSeconds = this.cachedExpiresAt ? this.cachedExpiresAt - now : 0;

    return {
      hasCache: !!this.cachedToken,
      expiresAt: this.cachedExpiresAt,
      remainingSeconds: Math.max(0, remainingSeconds),
      isValid: this.isTokenValid(),
    };
  }
}

// シングルトンインスタンスをエクスポート
export const ephemeralTokenManager = new EphemeralTokenManager();
