const BRIDGE_BASE = 'http://127.0.0.1:32111';
const COMMAND_INTERVAL_MS = 1500;
const CAPTURE_SNAPSHOT_TIMEOUT_MS = 20000;
const REALTIME_MESSAGE_TIMEOUT_MS = 6500;
const QUICK_CAPTURE_SNAPSHOT_TIMEOUT_MS = 6500;
const NMH_HOST = 'com.asa.chatgpt_nmh';
const NMH_READY_TIMEOUT_MS = 10000;

const state = {
  token: null,
  polling: false,
  bootstrapStarted: false,
  tabConversationIds: new Map(),
  nmhPort: null,
  nmhConnecting: false,
  // PL-024 CP4-CP6: widget click 元 tab を sticky 化、ASA→tab 全 round-trip command 経路の唯一の信頼源。
  // 旧 lastReadyTabId (heartbeat 経由 cache) + getActive*TabId (lastFocusedWindow filter) は CP6 で完全撤去。
  widgetSessionTabId: null,
};

function preferredCommandTabId() {
  return typeof state.widgetSessionTabId === 'number' ? state.widgetSessionTabId : null;
}

const CONVERSATION_ID_RE = /\/c\/([A-Za-z0-9\-]+)/;

function extractConversationIdFromUrl(url) {
  if (!url) {
    return null;
  }
  const m = CONVERSATION_ID_RE.exec(String(url));
  return m ? m[1] : null;
}

async function reportConversationIdIfChanged(tabId, tab) {
  const url = String((tab && tab.url) || '');
  const real = extractConversationIdFromUrl(url);
  if (!real) {
    return;
  }
  const prev = state.tabConversationIds.get(tabId);
  if (prev === real) {
    return;
  }
  state.tabConversationIds.set(tabId, real);
  await postEvent(
    'conversation_id_confirmed',
    {
      url,
      title: String((tab && tab.title) || 'ChatGPT_Conversation'),
    },
    {
      real_uuid: real,
      tab_id: tabId,
      observedAt: new Date().toISOString(),
    }
  );
}

async function fetchJson(url, init) {
  try {
    const response = await fetch(url, init);
    const text = await response.text();
    let data = {};
    try {
      data = text ? JSON.parse(text) : {};
    } catch (_error) {
      data = {};
    }
    return { ok: response.ok, status: response.status, data };
  } catch (_error) {
    return { ok: false, status: 0, data: {}, fetchError: true };
  }
}

async function fetchBridgeInfo() {
  const result = await fetchJson(`${BRIDGE_BASE}/bridge-info`, { cache: 'no-store' });
  if (!result.ok || !result.data || !result.data.token) {
    return null;
  }
  state.token = String(result.data.token);
  return result.data;
}

function connectNmh() {
  return new Promise((resolve, reject) => {
    let port;
    try {
      console.log('[ASA-NMH] connectNative start, host=', NMH_HOST);
      port = chrome.runtime.connectNative(NMH_HOST);
    } catch (err) {
      console.warn('[ASA-NMH] connectNative threw:', err);
      reject(err);
      return;
    }
    let settled = false;
    const finish = (fn, value) => {
      if (settled) return;
      settled = true;
      fn(value);
    };
    port.onMessage.addListener((msg) => {
      console.log('[ASA-NMH] recv:', msg);
      if (msg && msg.type === 'ready') {
        finish(resolve, port);
      } else if (msg && msg.type === 'error') {
        try { port.disconnect(); } catch (_e) {}
        finish(reject, new Error(msg.message || 'nmh error'));
      }
    });
    port.onDisconnect.addListener(() => {
      const lastErr = chrome.runtime.lastError;
      console.warn('[ASA-NMH] disconnect, lastError=', lastErr && lastErr.message);
      finish(reject, new Error(lastErr ? lastErr.message : 'nmh disconnected before ready'));
    });
    setTimeout(() => {
      if (settled) return;
      console.warn('[ASA-NMH] ready timeout');
      try { port.disconnect(); } catch (_e) {}
      finish(reject, new Error('nmh ready timeout'));
    }, NMH_READY_TIMEOUT_MS);
  });
}

// PL-025 CP3 fix-A: ensureAsaRunning を singleton in-flight promise 化。
// PL-025 で widget_action sendMessage が 2 連 (save_pending + save_open) 化し、
// 旧実装は fetchBridgeInfo の await 中 nmhConnecting lock 未設定で並行 2 invocation が
// race 通過 → ASA 2 個 spawn の bug が顕在化。in-flight promise を share させて
// 並行 call 全部同 promise を await することで race 根治。
let _ensureAsaRunningPromise = null;

async function ensureAsaRunning() {
  if (state.token) {
    return true;
  }
  if (_ensureAsaRunningPromise) {
    return _ensureAsaRunningPromise;
  }
  _ensureAsaRunningPromise = (async () => {
    const info = await fetchBridgeInfo();
    if (info) {
      console.log('[ASA-NMH] bridge already up, skip spawn');
      return true;
    }
    state.nmhConnecting = true;
    try {
      const port = await connectNmh();
      // PL-029 CP4: 旧 nmhPort を disconnect してから上書き。chrome.runtime.connectNative で得た port は
      // 明示 disconnect しない限り alive 維持 = NMH helper (bat + python.exe = 2 process) も alive 維持。
      // 旧実装は `state.nmhPort = port;` 直接代入で旧 port 残置 → service tab close + 再 open 反復で
      // NMH helper process が累積する root。能動 disconnect で旧 helper の stdin EOF → exit させて回避。
      // PL-025 CP3 fix-C 「NMH disconnect ≠ ASA exit」と整合 (ASA 本体は別 process で alive 維持)。
      if (state.nmhPort) {
        try { state.nmhPort.disconnect(); } catch (_e) {}
        state.nmhPort = null;
      }
      state.nmhPort = port;
      port.onDisconnect.addListener(() => {
        if (state.nmhPort === port) {
          state.nmhPort = null;
        }
        // PL-025 CP3 fix-C: NMH host disconnect ≠ ASA exit。NMH stub は spawn 完了後すぐ exit する
        // 設計 (port.onMessage で `{type: 'ready', spawned: true, pid, port}` 返してから自身 disconnect)、
        // ASA 本体は別 process で alive、`/bridge-info` 取得も valid なまま。
        // 旧実装は disconnect で `state.token = null` reset していたが、次の sendMessage 時に fix-A の
        // `if (state.token) return true` を skip させ → fetchBridgeInfo + connectNmh 経路に再突入 →
        // NMH が既存 ASA detect せず 2 個目 spawn する root。token は維持し、ASA crash 時の token
        // stale 復旧は別経路 (postEvent 401/403 検出 + clear logic、別 PL 起票候補) で対応する。
      });
      const info2 = await fetchBridgeInfo();
      console.log('[ASA-NMH] post-spawn fetchBridgeInfo ok=', Boolean(info2));
      return Boolean(info2);
    } catch (err) {
      console.warn('[ASA-NMH] ensureAsaRunning failed:', err && err.message);
      return false;
    } finally {
      state.nmhConnecting = false;
    }
  })().finally(() => {
    _ensureAsaRunningPromise = null;
  });
  return _ensureAsaRunningPromise;
}

async function postEvent(eventName, source, payload, _retried = false) {
  if (!state.token) {
    return { ok: false, data: {} };
  }
  const result = await fetchJson(`${BRIDGE_BASE}/event`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      tool: 'chatgpt_asa',
      version: 1,
      token: state.token,
      event: eventName,
      source,
      payload,
    }),
  });
  // PL-028: ASA crash / stale token recovery。fetch 失敗 (ECONNREFUSED = HTTP server 消失) または
  // 403 (token mismatch、token file 削除等の限定 case) で state.token clear + ensureAsaRunning 再実行
  // + 1 回 retry。_retried guard で recursion 防止 (2 回目失敗は元 caller に return)。
  // 通常 4xx/5xx (payload error 等) は token clear せず素通し。
  if (!_retried && (result.fetchError === true || result.status === 0 || result.status === 403)) {
    console.warn('[ASA] postEvent stale token detected, status=', result.status, 'fetchError=', Boolean(result.fetchError));
    state.token = null;
    const ok = await ensureAsaRunning();
    if (ok && state.token) {
      return postEvent(eventName, source, payload, true);
    }
    return { ok: false, data: {} };
  }
  return { ok: Boolean(result.ok), data: result.data || {} };
}

async function postDiagnosticStatus(stateName, source = {}, details = {}) {
  return postEvent(
    'status',
    {
      url: source.url || '',
      title: source.title || 'ChatGPT_ASA Bridge',
    },
    {
      state: stateName,
      observedAt: new Date().toISOString(),
      details,
    }
  );
}

async function sendMessageWithTimeout(tabId, message, timeoutMs) {
  const limitMs = Math.max(1000, Number(timeoutMs) || CAPTURE_SNAPSHOT_TIMEOUT_MS);
  return await Promise.race([
    chrome.tabs.sendMessage(tabId, message),
    new Promise(resolve => {
      setTimeout(() => resolve(null), limitMs);
    }),
  ]);
}

async function requestSnapshotFromTab(timeoutMs) {
  const timeout = typeof timeoutMs === 'number' ? timeoutMs : CAPTURE_SNAPSHOT_TIMEOUT_MS;
  const tabId = preferredCommandTabId();
  if (typeof tabId !== 'number') return null;
  try {
    const injected = await ensureContentScript(tabId);
    if (!injected) return null;
    return await sendMessageWithTimeout(tabId, { type: 'capture_snapshot' }, timeout);
  } catch (_error) {
    return null;
  }
}

async function ensureContentScript(tabId) {
  try {
    await chrome.scripting.executeScript({
      target: { tabId },
      files: ['content.js'],
    });
    return true;
  } catch (_error) {
    return false;
  }
}

async function reloadChatGPTTabs() {
  try {
    const tabs = await chrome.tabs.query({ url: 'https://chatgpt.com/*' });
    for (const tab of tabs) {
      if (typeof tab.id === 'number') {
        try {
          const injected = await ensureContentScript(tab.id);
          if (!injected) {
            await chrome.tabs.reload(tab.id);
          }
        } catch (_error) {
        }
      }
    }
  } catch (_error) {
    return;
  }
}

async function probeExistingChatGPTTabs() {
  try {
    const tabs = await chrome.tabs.query({ url: 'https://chatgpt.com/*' });
    for (const tab of tabs) {
      if (typeof tab.id === 'number') {
        await probeChatGPTTab(tab.id, String(tab.url || ''));
      }
    }
  } catch (_error) {
    return;
  }
}

function scheduleStartupProbeRetries() {
  const delays = [1000, 3000, 5000];
  for (const delay of delays) {
    setTimeout(() => {
      void probeExistingChatGPTTabs();
    }, delay);
  }
}

function bootstrapExistingTabs() {
  if (state.bootstrapStarted) {
    return;
  }
  state.bootstrapStarted = true;
  void (async () => {
    await probeExistingChatGPTTabs();
    scheduleStartupProbeRetries();
  })();
}

async function probeChatGPTTab(tabId, tabUrl) {
  // PL-029: tab probe = ASA 必要性 signal の pre-warm trigger。fire-and-forget で probe block しない、
  // singleton (PL-025 CP3 fix-A) + state.token early return で重複 spawn / 副作用 0。
  // tab probe 完了時点 ≈ ASA spawn 並行完了 = 1st widget click 時の体感 lag 解消。
  void ensureAsaRunning();
  const source = { url: tabUrl || '', title: 'ChatGPT_ASA Bridge' };
  try {
    const injected = await ensureContentScript(tabId);
    if (!injected) {
      await postDiagnosticStatus('content_injection_failed', source, { tabId });
      return;
    }
    const pong = await chrome.tabs.sendMessage(tabId, { type: 'ping_content' });
    if (pong && pong.ok) {
      await postDiagnosticStatus('content_ping_ok', { url: pong.url || tabUrl || '', title: pong.title || 'ChatGPT_Conversation' }, { tabId });
      return;
    }
    await postDiagnosticStatus('content_ping_empty', source, { tabId });
  } catch (_error) {
    await postDiagnosticStatus('content_ping_failed', source, { tabId });
  }
}

async function requestDomMessagesFromTab() {
  const tabId = preferredCommandTabId();
  if (typeof tabId !== 'number') {
    await postDiagnosticStatus('dom_messages_no_tab', {}, {});
    return null;
  }
  try {
    const injected = await ensureContentScript(tabId);
    if (!injected) {
      await postDiagnosticStatus('dom_messages_injection_failed', {}, { tabId });
      return null;
    }
    const result = await sendMessageWithTimeout(tabId, { type: 'get_dom_messages' }, 5000);
    if (!result) {
      await postDiagnosticStatus('dom_messages_empty', {}, { tabId });
      return null;
    }
    return result;
  } catch (_error) {
    await postDiagnosticStatus('dom_messages_failed', {}, { tabId });
    return null;
  }
}

async function pollCommands() {
  if (state.polling) {
    return;
  }
  state.polling = true;
  try {
    if (!state.token) {
      await fetchBridgeInfo();
    }
    if (!state.token) {
      return;
    }
    const result = await fetchJson(`${BRIDGE_BASE}/command/next?token=${encodeURIComponent(state.token)}`, { cache: 'no-store' });
    const command = result.data && result.data.command;
    if (!command) {
      return;
    }
    if (command.command === 'capture_snapshot' || command.command === 'capture_snapshot_quick') {
      const snapshot = await requestSnapshotFromTab(
        command.command === 'capture_snapshot_quick'
          ? QUICK_CAPTURE_SNAPSHOT_TIMEOUT_MS
          : CAPTURE_SNAPSHOT_TIMEOUT_MS
      );
      if (!snapshot) {
        return;
      }
      await postEvent(
        'snapshot',
        {
          url: snapshot.url || '',
          title: snapshot.title || 'ChatGPT_Conversation',
        },
        {
          timestamp: snapshot.timestamp || new Date().toISOString(),
          messages: Array.isArray(snapshot.messages) ? snapshot.messages : [],
          pageState: snapshot.pageState || '',
        }
      );
    } else if (command.command === 'get_dom_messages') {
      const domResult = await requestDomMessagesFromTab();
      if (!domResult) {
        return;
      }
      // PL-024 CP5: title forward (scroll_snap で latest_status (heartbeat 経路) でなく click 元 tab title を使うため)。
      await postEvent(
        'dom_messages',
        { url: domResult.url || '', title: domResult.title || '' },
        {
          messages: Array.isArray(domResult.messages) ? domResult.messages : [],
          scrollTop: domResult.scrollTop || 0,
          scrollHeight: domResult.scrollHeight || 0,
          clientHeight: domResult.clientHeight || 0,
        }
      );
    } else if (command.command === 'get_realtime_buffer') {
      const tabId = preferredCommandTabId();
      if (typeof tabId !== 'number') {
        return;
      }
      const injected = await ensureContentScript(tabId);
      if (!injected) {
        return;
      }
      const snapshot = await sendMessageWithTimeout(tabId, { type: 'get_realtime_buffer' }, REALTIME_MESSAGE_TIMEOUT_MS);
      if (!snapshot) {
        return;
      }
      await postEvent(
        'snapshot',
        {
          url: snapshot.url || '',
          title: snapshot.title || 'ChatGPT_Conversation',
        },
        {
          timestamp: snapshot.timestamp || new Date().toISOString(),
          messages: Array.isArray(snapshot.messages) ? snapshot.messages : [],
          pageState: snapshot.pageState || '',
          debug: snapshot.debug && typeof snapshot.debug === 'object' ? snapshot.debug : {},
        }
      );
    } else if (command.command === 'start_realtime_capture' || command.command === 'stop_realtime_capture') {
      const tabId = preferredCommandTabId();
      if (typeof tabId !== 'number') {
        return;
      }
      const injected = await ensureContentScript(tabId);
      if (!injected) {
        return;
      }
      await sendMessageWithTimeout(tabId, { type: command.command }, 3000);
    }
  } catch (_error) {
    return;
  } finally {
    state.polling = false;
  }
}

chrome.runtime.onInstalled.addListener(() => {
  void (async () => {
    await reloadChatGPTTabs();
  })();
});

chrome.runtime.onStartup.addListener(() => {
  void (async () => {
    await probeExistingChatGPTTabs();
    scheduleStartupProbeRetries();
  })();
});

bootstrapExistingTabs();

chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  if (!message || typeof message !== 'object') {
    return false;
  }
  if (message.type === 'bridge_heartbeat') {
    void (async () => {
      const result = await postEvent(
        'status',
        { url: message.url || '', title: message.title || 'ChatGPT_Conversation' },
        {
          state: message.pageState || 'bridge_heartbeat',
          observedAt: message.observedAt || new Date().toISOString(),
        }
      );
      const widgetState = (result && result.data && result.data.widget_state) || {};
      sendResponse({ ok: true, widget_state: widgetState });
    })();
    void pollCommands();
    return true;
  }
  if (message.type === 'widget_action') {
    // PL-024 CP4/CP5: widget click 元 tab を sticky 化 (round-trip command の routing 決定的化)。
    if (sender && sender.tab && typeof sender.tab.id === 'number') {
      state.widgetSessionTabId = sender.tab.id;
    }
    void (async () => {
      await ensureAsaRunning();
      const action = String(message.action || '');
      const payload = (message.payload && typeof message.payload === 'object') ? message.payload : {};
      const result = await postEvent(
        'widget_' + action,
        { url: message.url || '', title: message.title || 'ChatGPT_Conversation' },
        payload
      );
      const widgetState = (result && result.data && result.data.widget_state) || {};
      sendResponse({ ok: Boolean(result && result.ok), widget_state: widgetState });
    })();
    return true;
  }
  if (message.type === 'asa_image') {
    // PL-078: 画像 1 枚を別便で ASA へ (本体スナップショットは img_ref のみ=軽量)。
    // ASA は event='image' を受けて key→data_uri を貯め、 snapshot に img_ref で合流。
    void (async () => {
      await ensureAsaRunning();
      const key = String(message.key || '');
      const dataUri = String(message.data_uri || '');
      if (!key || !dataUri) { sendResponse({ ok: false }); return; }
      const result = await postEvent(
        'image',
        { url: message.url || '', title: 'ChatGPT_Conversation' },
        { key, data_uri: dataUri, conv_id: message.conv_id || '' }
      );
      sendResponse({ ok: Boolean(result && result.ok) });
    })();
    return true;
  }
  if (message.type === 'content_ready') {
    void postEvent(
      'status',
      { url: message.url || '', title: message.title || 'ChatGPT_Conversation' },
      { state: 'content_ready', observedAt: message.observedAt || new Date().toISOString() }
    );
    void pollCommands();
    sendResponse({ ok: true });
    return true;
  }
  if (message.type === 'page_status') {
    void postEvent(
      'status',
      { url: message.url || '', title: message.title || 'ChatGPT_Conversation' },
      {
        state: message.pageState || 'unknown',
        observedAt: message.observedAt || new Date().toISOString(),
        details: message.details && typeof message.details === 'object' ? message.details : {},
      }
    );
    void pollCommands();
    sendResponse({ ok: true });
    return true;
  }
  if (message.type === 'tab_closed') {
    // PL-021 CP2: pagehide は tab 単位 fire = 残 service tab があるなら post 抑止 (連鎖閉鎖回避)。
    const senderTabId = sender && sender.tab && typeof sender.tab.id === 'number' ? sender.tab.id : null;
    const payload = { url: message.url || '', title: message.title || 'ChatGPT_Conversation' };
    const meta = { observedAt: message.observedAt || new Date().toISOString() };
    chrome.tabs.query({ url: 'https://chatgpt.com/*' }, (tabs) => {
      const remaining = (tabs || []).filter(
        (t) => typeof t.id === 'number' && t.id !== senderTabId
      ).length;
      if (remaining === 0) {
        void postEvent('tab_closed', payload, meta);
        // PL-029 CP5: 残 service tab 0 = ASA shutdown 確定。NMH port も能動 disconnect で
        // helper exit + state.token reset。残置 NMH (Python 2) を回避し、次 service tab open
        // 時の ensureAsaRunning が token null = full spawn 経路を pre-warm 段階で発火、
        // postEvent recovery 待ちの 1st click lag を解消。
        if (state.nmhPort) {
          try { state.nmhPort.disconnect(); } catch (_e) {}
          state.nmhPort = null;
        }
        state.token = null;
      }
    });
    sendResponse({ ok: true });
    return true;
  }
  return false;
});

chrome.tabs.onRemoved.addListener((tabId) => {
  if (state.widgetSessionTabId === tabId) {
    state.widgetSessionTabId = null;
  }
  state.tabConversationIds.delete(tabId);
});

chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
  if (String((tab && tab.url) || '').startsWith('https://chatgpt.com/')) {
    void postDiagnosticStatus('tab_update_seen', { url: String(tab.url || ''), title: String(tab.title || 'ChatGPT_Conversation') }, {
      tabId,
      status: String(changeInfo.status || ''),
    });
  }
  // URL 変化を検知してクラッシュセーフティバッファの conversation_id を確定通知する。
  if (changeInfo.url !== undefined || changeInfo.status === 'complete') {
    if (String((tab && tab.url) || '').startsWith('https://chatgpt.com/')) {
      void reportConversationIdIfChanged(tabId, tab);
    }
  }
  if (changeInfo.status !== 'complete') {
    return;
  }
  const tabUrl = String((tab && tab.url) || '');
  if (!tabUrl.startsWith('https://chatgpt.com/')) {
    return;
  }
  void probeChatGPTTab(tabId, tabUrl);
});

setInterval(() => {
  void pollCommands();
}, COMMAND_INTERVAL_MS);