// PL-038 Phase 3: ChatGPT lazy mount + lazy attribute attachment 対策の API hook。
// MAIN world で document_start 時に install され、ChatGPT site 自身の
// /backend-api/conversation/<cid> fetch (page load 時 site が auto-fetch) を
// passive sniff で capture、 完全な mapping JSON を cache 化。 save click 時
// content.js は cache から messages を parse する経路に切替、 auto-scroll は
// 一切走らせない (PL-036 §invariant #4 整合)。
//
// active replay: cache miss (page load 後に fetch されてない or expire) 時、
// content.js から postMessage で replay request、 page_hook 側で saved Request
// (site が使った Request 全体 = 認証 header 付帯) を clone して再 fetch。
// 単純 fetch('/backend-api/conversation/<cid>') は site の auth chain を経由
// しないため 404 (probe で検証済)、 saved Request 経由のみが信頼可能。

(function () {
  if (window.__CHATGPT_ASA_PAGE_HOOK_INSTALLED__) {
    return;
  }
  window.__CHATGPT_ASA_PAGE_HOOK_INSTALLED__ = true;

  const MESSAGE_SOURCE = 'asa-chatgpt-page-hook';
  const MESSAGE_TYPE_CACHE_NOTIFY = 'cache-notify';
  const MESSAGE_TYPE_READ_REQUEST = 'cache-read-request';
  const MESSAGE_TYPE_READ_RESPONSE = 'cache-read-response';
  const MESSAGE_TYPE_REPLAY_REQUEST = 'replay-request';
  const MESSAGE_TYPE_REPLAY_RESPONSE = 'replay-response';
  const MESSAGE_TYPE_CACHE_CLEAR = 'cache-clear-request';

  const conversationCache = Object.create(null);
  const savedRequests = Object.create(null);

  const CONVERSATION_URL_RE = /\/backend-api\/conversation\/([0-9a-fA-F-]{8,})(?:[?#]|$)/;

  function extractCid(url) {
    try {
      const m = String(url || '').match(CONVERSATION_URL_RE);
      return m ? m[1] : null;
    } catch (_e) {
      return null;
    }
  }

  function shouldCapture(url, method) {
    if (String(method || 'GET').toUpperCase() !== 'GET') return false;
    return CONVERSATION_URL_RE.test(String(url || ''));
  }

  function postCacheNotify(cid, source) {
    try {
      window.postMessage({
        source: MESSAGE_SOURCE,
        type: MESSAGE_TYPE_CACHE_NOTIFY,
        cid,
        source_kind: source,
        fetched_at: Date.now(),
      }, location.origin);
    } catch (_e) {}
  }

  const originalFetch = window.fetch;
  window.fetch = async function (...args) {
    const input = args[0];
    const init = args[1] || {};
    const url = typeof input === 'string' ? input : (input && input.url) || '';
    const method = (init.method || (input && input.method) || 'GET').toUpperCase();
    let savedInput = null;
    if (shouldCapture(url, method)) {
      try {
        if (typeof Request !== 'undefined' && input instanceof Request) {
          savedInput = input.clone();
        } else {
          savedInput = input;
        }
      } catch (_e) {}
    }
    const response = await originalFetch.apply(this, args);
    if (shouldCapture(url, method)) {
      try {
        const cid = extractCid(url);
        if (cid && savedInput) {
          savedRequests[cid] = { input: savedInput, init: init || {} };
        }
        if (cid && response.status === 200) {
          const cloned = response.clone();
          cloned.text().then((text) => {
            conversationCache[cid] = {
              resp_text: String(text || ''),
              status: response.status,
              fetched_at: Date.now(),
              source_kind: 'passive_sniff',
            };
            postCacheNotify(cid, 'passive_sniff');
          }).catch(() => {});
        }
      } catch (_e) {}
    }
    return response;
  };

  const originalOpen = XMLHttpRequest.prototype.open;
  const originalSend = XMLHttpRequest.prototype.send;
  XMLHttpRequest.prototype.open = function (method, url, ...rest) {
    this.__asaChatGPTHookUrl = url;
    this.__asaChatGPTHookMethod = method;
    return originalOpen.call(this, method, url, ...rest);
  };
  XMLHttpRequest.prototype.send = function (...args) {
    const url = this.__asaChatGPTHookUrl || '';
    const method = String(this.__asaChatGPTHookMethod || 'GET').toUpperCase();
    if (shouldCapture(url, method)) {
      this.addEventListener('load', function () {
        try {
          const cid = extractCid(url);
          if (cid && this.status === 200) {
            conversationCache[cid] = {
              resp_text: String(this.responseText || ''),
              status: this.status,
              fetched_at: Date.now(),
              source_kind: 'passive_sniff_xhr',
            };
            postCacheNotify(cid, 'passive_sniff_xhr');
          }
        } catch (_e) {}
      });
    }
    return originalSend.apply(this, args);
  };

  // PL-045 修正: 競合拡張 (= ilmdofdhpnhffldihboadndccenlnfll) の bundle 精読で
  // 判明、 competitor は site の Authorization Bearer token のみ抽出 + 最小 headers
  // で /backend-api/conversation/<cid> を自前 fetch。 ASA は site の saved Request
  // 全体を clone してたため、 site が付与する conditional GET headers
  // (If-None-Match / cache hint 等) を含んだ replay 経路で OpenAI server から
  // 304 / cached stale response 受け取り realtime buffer 末尾 1 件取れない症状。
  // 対策: saved Request から Authorization 抽出 + 最小 headers + cache:'no-store'
  // で fetch、 site cache 経路から離脱。 cookies (= credentials:'include') は site
  // session 維持に必須なので含める。
  function extractAuthHeader(saved) {
    try {
      if (typeof Request !== 'undefined' && saved.input instanceof Request) {
        const h = saved.input.headers.get('Authorization');
        if (h) return h;
      }
    } catch (_e) {}
    try {
      const h = saved.init && saved.init.headers;
      if (h) {
        if (typeof Headers !== 'undefined' && h instanceof Headers) {
          const v = h.get('Authorization');
          if (v) return v;
        } else if (typeof h === 'object') {
          return h.Authorization || h.authorization || null;
        }
      }
    } catch (_e) {}
    return null;
  }

  function extractSavedUrl(saved) {
    try {
      if (typeof Request !== 'undefined' && saved.input instanceof Request) {
        return saved.input.url;
      }
      if (typeof saved.input === 'string') return saved.input;
    } catch (_e) {}
    return '';
  }

  // PL-045 新規 conv fallback: site が新規会話で /backend-api/conversation/<cid>
  // を GET 叩かない (= POST + streaming で進行、 URL は pushState で変化)
  // ため savedRequests が空、 既存 replay path だと no_saved_request で失敗。
  // 競合拡張 inj.js は /api/auth/session から accessToken を抽出して自前 fetch
  // しており、 ASA も同経路を fallback として採用。 saved 不在時のみ発動、
  // 既存 conv path には影響なし。
  async function getAccessToken() {
    try {
      const resp = await originalFetch.call(window, '/api/auth/session', {
        credentials: 'include',
        cache: 'no-store',
      });
      if (resp.status !== 200) return null;
      const data = await resp.json();
      return (data && data.accessToken) || null;
    } catch (_e) {
      return null;
    }
  }

  async function replayConversationFetch(cid) {
    const saved = savedRequests[cid];
    let authHeader = null;
    let url = '';
    if (saved) {
      authHeader = extractAuthHeader(saved);
      url = extractSavedUrl(saved);
    }
    let sourceKind = 'active_replay_minimal';
    if (!authHeader) {
      const token = await getAccessToken();
      if (token) {
        authHeader = `Bearer ${token}`;
        sourceKind = 'active_replay_token_fallback';
      }
    }
    if (!url) {
      url = `${location.origin}/backend-api/conversation/${cid}`;
    }
    if (!authHeader) {
      return { ok: false, reason: 'no_auth', status: null };
    }
    try {
      const response = await originalFetch.call(window, url, {
        method: 'GET',
        headers: { 'Authorization': authHeader },
        credentials: 'include',
        cache: 'no-store',
      });
      if (response.status === 200) {
        const text = await response.text();
        conversationCache[cid] = {
          resp_text: String(text || ''),
          status: response.status,
          fetched_at: Date.now(),
          source_kind: sourceKind,
        };
        postCacheNotify(cid, sourceKind);
        return { ok: true, status: response.status };
      }
      return { ok: false, reason: 'http_error', status: response.status };
    } catch (e) {
      return { ok: false, reason: 'fetch_threw', status: null, error: String(e) };
    }
  }

  window.addEventListener('message', (event) => {
    if (event.source !== window) return;
    const data = event.data;
    if (!data || data.source !== MESSAGE_SOURCE) return;
    if (data.type === MESSAGE_TYPE_READ_REQUEST) {
      const cid = data.cid;
      const reqId = data.reqId;
      const entry = (cid && conversationCache[cid]) || null;
      try {
        window.postMessage({
          source: MESSAGE_SOURCE,
          type: MESSAGE_TYPE_READ_RESPONSE,
          reqId,
          cid,
          entry,
          has_saved_request: !!(cid && savedRequests[cid]),
        }, location.origin);
      } catch (_e) {}
    } else if (data.type === MESSAGE_TYPE_REPLAY_REQUEST) {
      const cid = data.cid;
      const reqId = data.reqId;
      replayConversationFetch(cid).then((result) => {
        try {
          window.postMessage({
            source: MESSAGE_SOURCE,
            type: MESSAGE_TYPE_REPLAY_RESPONSE,
            reqId,
            cid,
            result,
          }, location.origin);
        } catch (_e) {}
      });
    } else if (data.type === MESSAGE_TYPE_CACHE_CLEAR) {
      // PL-045 codex review #3 派生: realtime stop 時 cache reset で stale 残置防止。
      // 通常保存は realtime 中 block 仕様だが、 stop 直後の widget click で stale を
      // 引く corner case を局所的に塞ぐ。 1-shot replay よりも network fetch 0 で
      // BAN 検知刺激ゼロ、 user 提案。 次 widget click は cache miss 経由で
      // fresh fetch (= minimal fetch + token fallback path 経由) に合流。
      const cid = data.cid;
      if (cid) {
        delete conversationCache[cid];
        delete savedRequests[cid];
      }
    }
  });
})();
