/**
 * Text Chat Controller
 *
 * NS `neccospeak/v1/chat` へのテキスト送信・応答受信を管理する。
 * pocxr 本番の Chat Completions SSE ワイヤ形式とは互換ではないため、
 * このコントローラは NS 固有のワイヤアダプタを内蔵する (計画書 §3.3)。
 *
 * テキストモードのコア機能:
 * - リクエスト送信（fetch、`credentials:'same-origin'` 必須 — INV-T7）
 * - レスポンスの Content-Type で分岐:
 *   - `application/json` → `{reply}` を一括表示
 *   - `text/event-stream` → NS ワイヤパーサ (イベント名なし `data:{"d":"…"}` の
 *     逐次チャンク + `event:done data:{"reply":"…"}` の確定フレーム)
 * - ストリーミング逐次表示
 * - 会話履歴（messages[]）管理
 * - event: action の処理（navigate → postMessage, prefill → postMessage）
 *   ※ NS の SSE には現状 action フレームが無い (P2 で追加検討 — Phase A では未実装)
 */

import { uuidv4 } from "../utils/uuid.js";
import { t } from "../ns/ns-rest.js";

// SSE 読取中、N ms 連続で chunk が来なければ silent hang とみなして abort する (Issue #526)。
// NS はハートビート (`: ping`) を送らないため、長考モデルでの誤 abort を避けるべく
// `window.__AGENT_CONFIG__.resilience.timeoutMs` が設定されていればそれを採用する
// (このモジュール既定の 60 秒がベースライン) — §3.3 の 6.
const SSE_IDLE_TIMEOUT_MS = 60_000;

function restBase() {
  const base = (typeof window !== "undefined" && window.__NS_REST__) || "/wp-json/neccospeak/v1/";
  return base.endsWith("/") ? base : `${base}/`;
}

function defaultApiUrl() {
  return `${restBase()}chat?stream=1`;
}

function resolveIdleTimeoutMs() {
  const cfg = (typeof window !== "undefined" && window.__AGENT_CONFIG__) || {};
  const configured = cfg.resilience && cfg.resilience.timeoutMs;
  return typeof configured === "number" && configured > 0 ? configured : SSE_IDLE_TIMEOUT_MS;
}

/** HTTP ステータス → i18n エラー文言 */
function errorMessageForStatus(status) {
  if (status === 429) return t("err_rate_limited", "We are experiencing high traffic. Please wait a moment and try again.");
  if (status === 503) return t("err_service_unavailable", "The service is currently unavailable. Please wait a moment and try again.");
  if (status === 403) return t("err_forbidden", "The request was rejected. Please reload the page and try again.");
  return t("err_connect_failed", "A connection problem occurred. Please wait a moment and try again.");
}

export class TextChatController {
  /**
   * @param {object} options
   * @param {string} [options.apiUrl] - NS chat エンドポイント (既定: `${restUrl}chat?stream=1`)
   * @param {Function} options.onTextChunk - テキストチャンク受信コールバック (content: string)
   * @param {Function} options.onTextDone - テキスト応答完了コールバック (fullText: string, doneData: object|null, meta?: object)
   * @param {Function} options.onAction - アクション受信コールバック (action: object)
   * @param {Function} options.onError - エラーコールバック (error: string)
   * @param {Function} options.onStreamStart - ストリーム開始コールバック
   * @param {Function} options.onStreamEnd - ストリーム終了コールバック
   */
  constructor(options = {}) {
    this.apiUrl = options.apiUrl || defaultApiUrl();
    this.onTextChunk = options.onTextChunk || (() => {});
    this.onTextDone = options.onTextDone || (() => {});
    this.onAction = options.onAction || (() => {});
    this.onError = options.onError || (() => {});
    this.onStreamStart = options.onStreamStart || (() => {});
    this.onStreamEnd = options.onStreamEnd || (() => {});

    // 会話履歴（最大50件 — 超過時は古いメッセージを切り捨て）
    this.messages = [];
    this.maxMessages = 50;

    // 現在のストリーミング状態
    this.isStreaming = false;
    this.abortController = null;

    // ユーザーコンテキスト
    this.userId = null;
    this.sessionId = this._generateSessionId();
    this.pageContext = null;
    this.location = null;

    // モード切替時の会話要約（音声→テキスト切替時に設定）
    this.conversationSummary = null;
  }

  /**
   * クライアント側でテキストセッション用UUIDを生成
   */
  _generateSessionId() {
    return uuidv4();
  }

  _isUuid(value) {
    return typeof value === "string" &&
      /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value);
  }

  /**
   * 新しいテキストセッションを開始（モード切替時に呼ばれる）
   */
  startNewSession() {
    this.sessionId = this._generateSessionId();
    return this.sessionId;
  }

  /**
   * ユーザーコンテキストを設定
   */
  setContext({ userId, sessionId, pageContext, location }) {
    if (userId !== undefined) this.userId = userId;
    if (sessionId !== undefined) {
      this.sessionId = this._isUuid(sessionId) ? sessionId : this._generateSessionId();
    }
    if (pageContext !== undefined) this.pageContext = pageContext;
    if (location !== undefined) this.location = location;
  }

  /**
   * ページコンテキストを更新（pjax遷移時に呼ばれる）
   */
  updatePageContext(pageContext) {
    this.pageContext = pageContext;
  }

  /**
   * メッセージを送信し、NS `/chat` から応答を受信する。
   * レスポンスの Content-Type によって JSON 一括表示 / SSE 逐次表示に分岐する。
   * @param {string} userMessage - ユーザーの入力テキスト
   * @param {object} [options] - オプション
   * @param {boolean} [options.ephemeral] - true の場合、送信後にユーザーメッセージを履歴から除去（挨拶トリガー等）
   * @returns {Promise<string>} AI の完全応答テキスト
   */
  async sendMessage(userMessage, options = {}) {
    if (this.isStreaming) {
      console.warn("[TextChat] Already streaming, ignoring send");
      return "";
    }

    // 会話履歴に追加（上限超過時は古いメッセージを切り捨て）
    this.messages.push({ role: "user", content: userMessage });
    if (this.messages.length > this.maxMessages) {
      this.messages = this.messages.slice(-this.maxMessages);
    }

    this.isStreaming = true;
    this.abortController = new AbortController();
    this.onStreamStart();

    let fullText = "";
    let doneData = null;
    // 「idle timeout で abort した」を「ユーザー操作で abort した」と区別するためのフラグ。
    // 前者はユーザーにエラー表示、後者は silent ((既存挙動維持)) で扱い分ける。
    let idleTimeoutFired = false;
    let idleTimer = null;
    const idleTimeoutMs = resolveIdleTimeoutMs();
    const resetIdleTimer = () => {
      if (idleTimer) clearTimeout(idleTimer);
      idleTimer = setTimeout(() => {
        idleTimeoutFired = true;
        this.abortController?.abort();
      }, idleTimeoutMs);
    };

    // 2026-08 撤去記録: ここにあった F9 (Batch H) の userTurnCount 算出
    // (_voiceOrigin を除いた「実際にテキストモードで送信したユーザーターン数」)
    // は、下記の sid/turn 送信撤去(判断事項C)に伴い唯一の消費者を失ったため
    // 削除した。

    try {
      const response = await fetch(this.apiUrl, {
        method: "POST",
        credentials: "same-origin",
        headers: {
          "Content-Type": "application/json; charset=utf-8",
        },
        body: JSON.stringify({
          // F9 (Batch H): 内部管理用フィールド (_voiceOrigin 等) を送信前に必ず
          // 落とし、/chat へ渡す各要素を {role, content} のみに正規化する
          // (rest.php のバリデーション/INV を壊さないため)。
          messages: this.messages.map((m) => ({ role: m.role, content: m.content })),
          post_id: (typeof window !== "undefined" && window.__AGENT_CONFIG__ && window.__AGENT_CONFIG__.page_id) || 0,
          // 2026-08 撤去記録: ここにあった sid/turn (会話ログとの相関用フィールド)
          // は、会話ログ機能撤去(判断事項C)に伴い送信を止めた。rest.php の
          // neccospeak_normalize_chat_input() は元々これらのキーを読まないため、
          // 送らなくなってもリクエストの解釈は変わらない。
        }),
        signal: this.abortController.signal,
      });

      if (!response.ok) {
        const message = errorMessageForStatus(response.status);
        throw Object.assign(new Error(message), { httpStatus: response.status, isI18n: true });
      }

      const contentType = (response.headers && response.headers.get
        ? (response.headers.get("Content-Type") || response.headers.get("content-type") || "")
        : "");

      if (contentType.indexOf("text/event-stream") === -1) {
        // 非ストリーム: `{reply}` を一括表示 (Phase A は typewriter 演出なし)
        const data = await response.json();
        fullText = data && typeof data.reply === "string" ? data.reply : "";
        doneData = data;
        if (fullText) {
          this.onTextChunk(fullText);
        }
      } else {
        // SSE ストリームを読み取り (NS ワイヤ形式: イベント名なし data:{"d":"…"})
        const reader = response.body.getReader();
        const decoder = new TextDecoder();
        let buffer = "";
        // SSE仕様: currentEvent はチャンクをまたいで保持される
        let currentEvent = "";

        // idle timer をストリーム読取開始時に起動。chunk 受信ごとに reset する。
        resetIdleTimer();

        while (true) {
          const { done, value } = await reader.read();
          if (done) break;
          // chunk が来た = サーバー応答生存。
          resetIdleTimer();

          buffer += decoder.decode(value, { stream: true });
          const lines = buffer.split("\n");
          buffer = lines.pop() || "";

          for (const line of lines) {
            // SSE仕様: 空行はイベントのディスパッチトリガー
            if (line.trim() === "") {
              currentEvent = "";
              continue;
            }

            if (line.startsWith("event: ") || line.startsWith("event:")) {
              currentEvent = line.slice(line.indexOf(":") + 1).trim();
              continue;
            }

            if (line.startsWith("data: ") || line.startsWith("data:")) {
              const dataStr = line.slice(line.indexOf(":") + 1).trim();
              let data;
              try {
                data = JSON.parse(dataStr);
              } catch {
                continue;
              }

              switch (currentEvent) {
                case "":
                  // NS: イベント名なしの逐次チャンク { d: "…" }
                  if (data && typeof data.d === "string") {
                    fullText += data.d;
                    this.onTextChunk(data.d);
                  }
                  break;

                case "action":
                  // P2 (未実装 Phase A): 将来 NS が action フレームを追加した場合の後方互換
                  this.onAction(data);
                  break;

                case "error":
                  this.onError(data.message || data.error || "Unknown error");
                  break;

                case "done":
                  // ストリーム完了。data.reply が逐次和と食い違う場合は data.reply を正とする
                  // (banned-word マスク等のサーバー側後処理を反映するため)。
                  doneData = data;
                  break;
              }
            }
          }
        }

        if (doneData && typeof doneData.reply === "string" && doneData.reply !== fullText) {
          fullText = doneData.reply;
        }
      }

      // 応答を会話履歴に追加
      if (fullText) {
        this.messages.push({ role: "assistant", content: fullText });
      }

      // ephemeral: トリガーメッセージを履歴から除去（AIの応答は残す）
      if (options.ephemeral) {
        this.messages = this.messages.filter(m => m.content !== userMessage || m.role !== "user");
      }

      this.onTextDone(fullText, doneData);
      return fullText;
    } catch (error) {
      if (error.name === "AbortError") {
        // idle timeout 経由の abort はユーザーにエラー表示。明示的 abort() (ユーザー操作) は silent。
        if (idleTimeoutFired) {
          const msg = t("err_idle_timeout", `No response arrived for ${Math.round(idleTimeoutMs / 1000)} seconds or more. Please try again.`);
          console.warn("[TextChat] SSE idle timeout — aborting");
          this.onError(msg);
        } else {
          console.log("[TextChat] Request aborted");
        }
        return fullText;
      }
      const message = error.isI18n ? error.message : errorMessageForStatus(error.httpStatus);
      console.error("[TextChat] Error:", error.message);
      this.onError(message);
      return fullText;
    } finally {
      if (idleTimer) clearTimeout(idleTimer);
      idleTimer = null;
      this.isStreaming = false;
      this.abortController = null;
      // 会話要約は初回送信後にクリア（stale summary の再送防止）
      this.conversationSummary = null;
      this.onStreamEnd();
    }
  }

  /**
   * 現在のストリーミングを中断
   */
  abort() {
    if (this.abortController) {
      this.abortController.abort();
    }
  }

  /**
   * 会話履歴をクリア
   */
  clearHistory() {
    this.messages = [];
  }

  /**
   * 会話履歴を取得
   * @returns {Array} messages 配列
   */
  getHistory() {
    return [...this.messages];
  }

  /**
   * 会話履歴を復元（セッション復元時に使用）
   * @param {Array} messages - 復元する messages 配列
   */
  restoreHistory(messages) {
    this.messages = [...messages];
  }

  /**
   * モード切替時の会話要約を取得
   * @returns {string} 直近の会話要約テキスト
   */
  getConversationSummary() {
    const recent = this.messages.slice(-6);
    return recent
      // "User"/"AI" are structural role markers inside an LLM-facing summary,
      // not UI copy — kept as plain ASCII labels (never translated) so both
      // sides of the pair stay symmetric (INV-12: no Japanese in source).
      .map((m) => `${m.role === "user" ? "User" : "AI"}: ${m.content.substring(0, 80)}`)
      .join("\n");
  }
}
