(() => {
  const ACTIONS = ['newfile', 'rewritefile', 'replacestring', 'replaceblock', 'replacefunction', 'insertbefore', 'insertafter'];

  const ACTION_PATTERNS = {
    rewritefile: {
      certain: ['drop in replacement', 'replace the whole file', 'replace the entire file', 'rewrite the full file', 'use this as the full file', 'full file replace'],
      likely: ['replace file', 'full file']
    },
    newfile: {
      certain: ['create a new file', 'new file'],
      likely: ['create file']
    },
    replacefunction: {
      certain: ['replace the full function', 'replace function', 'replace the complete function'],
      likely: ['function replacement', 'replace method']
    },
    replaceblock: {
      certain: ['replace this block', 'replace the block', 'between these lines', 'bounded region replacement'],
      likely: ['replace section', 'replace region']
    },
    insertbefore: {
      certain: ['insert before', 'add before', 'put this before'],
      likely: ['before this line', 'before the following']
    },
    insertafter: {
      certain: ['insert after', 'add after', 'put this after', 'insert these helpers after'],
      likely: ['after this line', 'after the following']
    },
    replacestring: {
      certain: ['look for', 'change it to', 'replace this line', 'replace these parts', 'replace the import line', 'replace the token line', 'use this exact replacement', 'should become'],
      likely: ['replace with', 'find', 'search for', 'locate', 'instead']
    }
  };

  const FILE_RE = /(?:[A-Za-z]:\\[^\s,;:()\]]+|(?:[\w.-]+[\\/])*[\w.-]+\.(?:ts|tsx|js|jsx|json|css|html|md|txt|go|py|java|rs|yaml|yml|toml|sh|bash))/g;
  const FENCE_RE_SOURCE = '```[^\\r\\n]*\\r?\\n([\\s\\S]*?)```';

  const state = {
    learnedPhrases: {},
    observer: null,
    lastSeenMessageKey: '',
    lastSeenMessageText: '',
    baselineReady: false,
    parseTimer: null,
    disabled: false
  };

  init();

  async function init() {
    const store = await chrome.storage.local.get(['learnedPhrases']);
    state.learnedPhrases = store.learnedPhrases || {};
    observeMessages();
    parseSoon();
    chrome.storage.onChanged.addListener((changes, area) => {
      if (area === 'local' && changes.learnedPhrases) {
        state.learnedPhrases = changes.learnedPhrases.newValue || {};
        parseSoon();
      }
    });
  }

  function observeMessages() {
    if (state.observer) state.observer.disconnect();
    state.observer = new MutationObserver(() => parseSoon());
    state.observer.observe(document.body, { childList: true, subtree: true, characterData: true });
  }

  function parseSoon() {
    if (state.disabled) return;
    clearTimeout(state.parseTimer);
    state.parseTimer = setTimeout(parseAndSend, 250);
  }

  function parseAndSend() {
    const messages = getAssistantMessages();
    const latestMessage = messages[messages.length - 1];
    if (!latestMessage) return;

    const latestText = extractText(latestMessage);
    const latestKey = getMessageKey(latestMessage, messages.length - 1);
    const firstScan = !state.baselineReady;

    if (firstScan) {
      state.baselineReady = true;
    }

    const hasNewTurn = latestKey !== state.lastSeenMessageKey;
    const hasNewContent = latestText !== state.lastSeenMessageText;
    if (!firstScan && !hasNewTurn && !hasNewContent) return;

    state.lastSeenMessageKey = latestKey;
    state.lastSeenMessageText = latestText;
    sendActivity('low');

    const candidates = parseTextCandidates(latestText);
    if (candidates.length) sendActivity('medium');

    if (!candidates.length) return;

    safeSendMessage({ type: 'NGO_UPDATE_CANDIDATES', candidates });
  }

  function parseTextCandidates(text) {
    const candidates = [];
    const raw = String(text || '');
    if (!raw.trim()) return candidates;

    const codeBlocks = extractCodeBlocks(raw);
    const fileCandidates = buildRecentFileCandidates(raw);

    dedupeInto(candidates, parseExplicitManifestBlocks(raw, fileCandidates));
    dedupeInto(candidates, parseRawManifestArrays(raw, fileCandidates));
    dedupeInto(candidates, parseRawManifestObjects(raw, fileCandidates));
    dedupeInto(candidates, parseTwoBlockPackets(raw, codeBlocks, fileCandidates));
    dedupeInto(candidates, parseSingleBlockPackets(raw, codeBlocks, fileCandidates));

    return candidates;
  }

  function getMessageKey(node, index) {
    if (!node) return `missing_${index}`;
    return node.getAttribute?.('data-message-id')
      || node.getAttribute?.('data-testid')
      || node.id
      || `${node.tagName || 'node'}_${index}`;
  }

  function getAssistantMessages() {
    const selectors = [
      '[data-message-author-role="assistant"]',
      '[data-testid="conversation-turn"]',
      'main article',
      '.font-claude-message',
      '.markdown',
      '.prose'
    ];

    const nodes = [];
    for (const selector of selectors) {
      for (const node of document.querySelectorAll(selector)) {
        if (!node || nodes.includes(node)) continue;
        if (selector === '[data-testid="conversation-turn"]' && node.querySelector('[data-testid="human-turn-icon"]')) continue;
        const text = extractText(node);
        if (!text.trim()) continue;
        nodes.push(node);
      }
      if (nodes.length) break;
    }

    return nodes;
  }

  function extractText(node) {
    return node?.innerText || node?.textContent || '';
  }

  function extractCodeBlocks(text) {
    const out = [];
    const fenceRe = new RegExp(FENCE_RE_SOURCE, 'g');
    let match;

    while ((match = fenceRe.exec(String(text || '')))) {
      out.push({
        code: String(match[1] || '').replace(/\n$/, ''),
        start: match.index,
        end: fenceRe.lastIndex
      });
    }

    return out.filter((entry) => entry.code.trim());
  }

  function parseExplicitManifestBlocks(text, fileCandidates) {
    const out = [];

    for (const block of extractCodeBlocks(text)) {
      const manifests = tryParseManifestCollection(block.code);
      if (!manifests.length) continue;

      manifests.forEach((manifest, index) => {
        out.push(
          buildExactManifestCandidate(
            manifest,
            `${block.code}_${index}`,
            fileCandidates,
            manifests.length > 1 ? 'Explicit manifest array detected' : 'Explicit manifest block detected'
          )
        );
      });
    }

    return out;
  }

  function parseRawManifestArrays(text, fileCandidates) {
    const out = [];
    const raw = String(text || '').trim();

    if (!raw.startsWith('[') || !raw.endsWith(']')) return out;

    const manifests = tryParseManifestCollection(raw);
    if (!manifests.length) return out;

    manifests.forEach((manifest, index) => {
      out.push(
        buildExactManifestCandidate(
          manifest,
          `${raw}_${index}`,
          fileCandidates,
          'Raw manifest array detected'
        )
      );
    });

    return out;
  }

  function parseRawManifestObjects(text, fileCandidates) {
    const out = [];
    const raw = String(text || '');
    const matches = extractBalancedJsonObjects(raw);

    for (const candidateText of matches) {
      if (!looksLikeManifestObject(candidateText)) continue;
      const manifest = tryParseManifest(candidateText);
      if (!manifest) continue;
      out.push(buildExactManifestCandidate(manifest, candidateText, fileCandidates, 'Explicit manifest text detected'));
    }

    return out;
  }

  function extractBalancedJsonObjects(text) {
    const raw = String(text || '');
    const out = [];
    let start = -1;
    let depth = 0;
    let inString = false;
    let escaped = false;

    for (let i = 0; i < raw.length; i += 1) {
      const ch = raw[i];

      if (inString) {
        if (escaped) {
          escaped = false;
        } else if (ch === '\\') {
          escaped = true;
        } else if (ch === '"') {
          inString = false;
        }
        continue;
      }

      if (ch === '"') {
        inString = true;
        continue;
      }

      if (ch === '{') {
        if (depth === 0) start = i;
        depth += 1;
        continue;
      }

      if (ch === '}') {
        if (depth > 0) depth -= 1;
        if (depth === 0 && start >= 0) {
          out.push(raw.slice(start, i + 1));
          start = -1;
        }
      }
    }

    return out;
  }

  function buildExactManifestCandidate(manifest, rawText, fileCandidates, evidenceLabel) {
    return {
      id: 'manifest_' + hashString(rawText),
      kind: 'manifest',
      uiType: 'manifest',
      task: String(manifest.Task || 'Apply manifest'),
      taskContextSentence: String(manifest.Task || 'Apply manifest'),
      action: String(manifest.Action || '').toLowerCase(),
      actionChoice: String(manifest.Action || '').toLowerCase(),
      actionConfidence: 'certain',
      actionContextSentence: evidenceLabel,
      payload: manifest.Payload,
      fileCandidates: manifest.FilePath ? [{ file: String(manifest.FilePath), evidence: ['Explicit FilePath in manifest'], fromTree: false }] : fileCandidates,
      evidenceSentences: [evidenceLabel],
      exactManifest: true,
      manifest,
      requiresReview: false
    };
  }

  function tryParseManifestCollection(raw) {
    try {
      const parsed = JSON.parse(raw);

      if (Array.isArray(parsed)) {
        return parsed
          .map((item) => normalizeManifestKeys(item))
          .filter((item) => {
            const action = String(item.Action || '').toLowerCase();
            if (!item.Action || !item.FilePath || item.Payload === undefined) return false;
            if (!ACTIONS.includes(action)) return false;
            if (!item.Task) item.Task = 'Apply manifest';
            return true;
          });
      }

      const single = tryParseManifest(raw);
      return single ? [single] : [];
    } catch {
      return [];
    }
  }

  function tryParseManifest(raw) {
    try {
      const parsed = JSON.parse(raw);
      if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return null;

      const normalized = normalizeManifestKeys(parsed);
      const action = String(normalized.Action || '').toLowerCase();

      if (!normalized.Action || !normalized.FilePath || normalized.Payload === undefined) return null;
      if (!ACTIONS.includes(action)) return null;

      if (!normalized.Task) normalized.Task = 'Apply manifest';

      return normalized;
    } catch {
      return null;
    }
  }

  function looksLikeManifestObject(raw) {
    const text = String(raw || '');
    return /"action"\s*:/i.test(text)
      && /"filepath"\s*:/i.test(text)
      && /"payload"\s*:/i.test(text);
  }

  function normalizeManifestKeys(parsed) {
    const out = {};

    for (const [key, value] of Object.entries(parsed || {})) {
      const normalizedKey = String(key).toLowerCase();
      if (normalizedKey === 'task') out.Task = value;
      else if (normalizedKey === 'filepath') out.FilePath = value;
      else if (normalizedKey === 'action') out.Action = value;
      else if (normalizedKey === 'payload') out.Payload = value;
      else if (normalizedKey === 'comment') out.Comment = value;
      else out[key] = value;
    }

    return out;
  }

  function parseTwoBlockPackets(text, codeBlocks, fileCandidates) {
    const out = [];
    if (codeBlocks.length < 2) return out;

    for (let i = 0; i < codeBlocks.length - 1; i += 2) {
      const first = codeBlocks[i];
      const second = codeBlocks[i + 1];
      if (!first || !second) continue;

      const context = buildContextWindow(text, first.start, second.end);
      const task = findTaskSentence(text, first.start) || 'Apply parsed change';
      const actionInfo = determineAction(context, { blockCount: 2, hasStrongPair: true });
      const payloadInfo = buildPayloadForAction(actionInfo.action, first.code, second.code, context);

      if (!payloadInfo.payload) continue;

      out.push({
        id: 'pair_' + hashString(task + '|' + actionInfo.action + '|' + first.code + '|' + second.code),
        kind: 'parsed',
        uiType: uiTypeFromAction(actionInfo.action),
        task,
        taskContextSentence: task,
        action: actionInfo.action,
        actionChoice: actionInfo.action,
        actionConfidence: actionInfo.confidence,
        actionContextSentence: context.actionSentence || task,
        payload: payloadInfo.payload,
        payloadContextSentence: context.actionSentence || task,
        fileCandidates: dedupeFileCandidates(collectPacketFileCandidates(context, fileCandidates)),
        evidenceSentences: context.recentSentences,
        sourceSnippet: context.actionSentence || task,
        codeBlocksUsed: [first.code, second.code],
        requiresReview: actionInfo.confidence !== 'certain'
      });
    }

    return out;
  }

  function parseSingleBlockPackets(text, codeBlocks, fileCandidates) {
    const out = [];
    if (!codeBlocks.length) return out;

    for (const block of codeBlocks) {
      const context = buildContextWindow(text, block.start, block.end);
      const anchorInfo = extractSingleBlockAnchor(context);
      if (!anchorInfo.valid) continue;

      const task = findTaskSentence(text, block.start) || 'Apply parsed change';
      const actionInfo = determineSingleBlockAction(context, anchorInfo);
      const payloadInfo = buildSingleBlockPayload(actionInfo.action, block.code, context, anchorInfo);
      if (!payloadInfo.payload) continue;

      out.push({
        id: 'single_' + hashString(task + '|' + actionInfo.action + '|' + anchorInfo.anchor + '|' + block.code),
        kind: 'parsed',
        uiType: uiTypeFromAction(actionInfo.action),
        task,
        taskContextSentence: task,
        action: actionInfo.action,
        actionChoice: actionInfo.action,
        actionConfidence: actionInfo.confidence,
        actionContextSentence: anchorInfo.sentence || task,
        payload: payloadInfo.payload,
        payloadContextSentence: anchorInfo.sentence || task,
        fileCandidates: dedupeFileCandidates(collectPacketFileCandidates(context, fileCandidates)),
        evidenceSentences: context.recentSentences,
        sourceSnippet: anchorInfo.sentence || task,
        codeBlocksUsed: [block.code],
        requiresReview: true
      });
    }

    return out;
  }

  function buildContextWindow(text, start, end) {
    const before = text.slice(Math.max(0, start - 900), start);
    const between = text.slice(start, end);
    const after = text.slice(end, Math.min(text.length, end + 450));
    const merged = `${before}\n${between}\n${after}`;
    const recentSentences = splitSentences(before).slice(-6);
    const actionSentence = findLikelyActionSentence([...recentSentences, ...splitSentences(after).slice(0, 2)]) || '';
    return { before, between, after, merged, recentSentences, actionSentence };
  }

  function findTaskSentence(text, beforeIndex) {
    const before = text.slice(Math.max(0, beforeIndex - 1200), beforeIndex);
    const sentences = splitSentences(before).slice(-8).reverse();
    const highlighted = sentences.find((sentence) => /[`"'“”‘’]/.test(sentence) && sentence.length > 8);
    if (highlighted) return highlighted.trim();
    const flavored = sentences.find((sentence) => hasActionFlavor(sentence));
    if (flavored) return flavored.trim();
    return sentences[0]?.trim() || '';
  }

  function findLikelyActionSentence(sentences) {
    return sentences.find((sentence) => hasActionFlavor(sentence)) || sentences[sentences.length - 1] || '';
  }

  function determineAction(context, options) {
    const lower = context.merged.toLowerCase();

    for (const [action, phrases] of Object.entries(state.learnedPhrases || {})) {
      if (!ACTIONS.includes(action)) continue;
      if ((phrases || []).some((phrase) => lower.includes(phrase))) {
        return { action, confidence: 'certain' };
      }
    }

    for (const action of ACTIONS) {
      const bucket = ACTION_PATTERNS[action];
      if (!bucket) continue;
      if ((bucket.certain || []).some((phrase) => lower.includes(phrase))) return { action, confidence: 'certain' };
    }

    for (const action of ACTIONS) {
      const bucket = ACTION_PATTERNS[action];
      if (!bucket) continue;
      if ((bucket.likely || []).some((phrase) => lower.includes(phrase))) return { action, confidence: 'likely' };
    }

    if (options?.hasStrongPair) return { action: 'replacestring', confidence: 'likely' };
    return { action: '', confidence: 'review' };
  }

  function determineSingleBlockAction(context, anchorInfo) {
    const lower = context.merged.toLowerCase();
    if (anchorInfo.type === 'insertbefore') return { action: 'insertbefore', confidence: 'likely' };
    if (anchorInfo.type === 'insertafter') return { action: 'insertafter', confidence: 'likely' };
    if ((ACTION_PATTERNS.replacefunction.certain || []).some((phrase) => lower.includes(phrase))) return { action: 'replacefunction', confidence: 'likely' };
    if ((ACTION_PATTERNS.replaceblock.certain || []).some((phrase) => lower.includes(phrase))) return { action: 'replaceblock', confidence: 'likely' };
    return { action: 'replacestring', confidence: 'review' };
  }

  function buildPayloadForAction(action, oldCode, newCode, context) {
    if (!action) return { payload: null };

    if (action === 'replacestring') {
      return { payload: { target: oldCode, replacement: newCode } };
    }

    if (action === 'replacefunction') {
      const start = extractFunctionAnchor(context) || firstNonEmptyLine(oldCode);
      if (!start) return { payload: null };
      return { payload: { start, replacement: newCode } };
    }

    if (action === 'replaceblock') {
      const start = extractBlockMarker(context) || firstNonEmptyLine(oldCode);
      const end = lastNonEmptyLine(oldCode);
      if (!start || !end) return { payload: null };
      return { payload: { start, end, replacement: newCode } };
    }

    if (action === 'insertbefore' || action === 'insertafter') {
      const find = firstMeaningfulAnchor(oldCode) || extractInsertAnchor(context);
      if (!find) return { payload: null };
      return { payload: { find, content: newCode } };
    }

    if (action === 'rewritefile' || action === 'newfile') {
      return { payload: newCode };
    }

    return { payload: null };
  }

  function buildSingleBlockPayload(action, code, context, anchorInfo) {
    if (action === 'insertbefore' || action === 'insertafter') {
      const find = anchorInfo.anchor || extractInsertAnchor(context);
      if (!find) return { payload: null };
      return { payload: { find, content: code } };
    }

    if (action === 'replacefunction') {
      const start = anchorInfo.anchor || extractFunctionAnchor(context) || firstNonEmptyLine(code);
      if (!start) return { payload: null };
      return { payload: { start, replacement: code } };
    }

    if (action === 'replaceblock') {
      const start = anchorInfo.anchor || extractBlockMarker(context) || firstNonEmptyLine(code);
      const end = lastNonEmptyLine(code);
      if (!start || !end) return { payload: null };
      return { payload: { start, end, replacement: code } };
    }

    if (action === 'replacestring') {
      const target = anchorInfo.anchor || extractFindAnchor(context);
      if (!target) return { payload: null };
      return { payload: { target, replacement: code } };
    }

    if (action === 'rewritefile' || action === 'newfile') {
      return { payload: code };
    }

    return { payload: null };
  }

  function extractSingleBlockAnchor(context) {
    const sentences = [...context.recentSentences, ...splitSentences(context.after).slice(0, 2)];

    for (const sentence of sentences) {
      const lower = sentence.toLowerCase();
      if (/(insert|add|put).+before/.test(lower)) return { valid: true, type: 'insertbefore', anchor: extractQuotedOrTail(sentence, 'before'), sentence };
      if (/(insert|add|put).+after/.test(lower)) return { valid: true, type: 'insertafter', anchor: extractQuotedOrTail(sentence, 'after'), sentence };
      if (/replace function|replace the full function/.test(lower)) return { valid: true, type: 'replacefunction', anchor: extractFunctionName(sentence), sentence };
      if (/replace this block|replace the block|between these lines/.test(lower)) return { valid: true, type: 'replaceblock', anchor: extractBlockMarker({ merged: sentence }) || extractQuotedOrTail(sentence, 'replace'), sentence };
      if (/look for|find|search for|locate/.test(lower)) return { valid: true, type: 'replacestring', anchor: extractFindAnchor({ recentSentences: [sentence], merged: sentence }), sentence };
    }

    return { valid: false, type: '', anchor: '', sentence: '' };
  }

  function extractFunctionAnchor(context) {
    const text = context.merged || '';
    const sentence = findLikelyActionSentence(splitSentences(text)) || '';
    return extractFunctionName(sentence);
  }

  function extractFunctionName(sentence) {
    const match = String(sentence || '').match(/([A-Za-z_$][\w$]*\([^)]*\)|[A-Za-z_$][\w$]*)/);
    return match?.[1] ? match[1].trim() : '';
  }

  function extractBlockMarker(context) {
    const text = String(context?.merged || '');
    const markerMatch = text.match(/(?:block marker|marker|section)\s+(?:such as\s+)?["'`]?([~#/@A-Za-z0-9_.:-]+)["'`]?/i);
    if (markerMatch?.[1]) return markerMatch[1].trim();
    const quoted = text.match(/["'`]([^"'`]*~ai[^"'`]*)["'`]/i);
    if (quoted?.[1]) return quoted[1].trim();
    const loose = text.match(/(~[A-Za-z0-9_.:-]+)/);
    if (loose?.[1]) return loose[1].trim();
    return '';
  }

  function extractInsertAnchor(context) {
    const text = context.merged || '';
    const beforeMatch = text.match(/before\s+["'`]?([^\n"'`]+)["'`]?/i);
    if (beforeMatch?.[1]) return beforeMatch[1].trim();
    const afterMatch = text.match(/after\s+["'`]?([^\n"'`]+)["'`]?/i);
    if (afterMatch?.[1]) return afterMatch[1].trim();
    return '';
  }

  function extractFindAnchor(context) {
    const text = context.merged || context.recentSentences?.join('\n') || '';
    const quoted = extractQuotedOrTail(text, 'find') || extractQuotedOrTail(text, 'look for') || extractQuotedOrTail(text, 'search for') || extractQuotedOrTail(text, 'locate');
    if (quoted) return quoted;
    return '';
  }

  function extractQuotedOrTail(text, keyword) {
    const quoted = String(text || '').match(/["'`]([^"'`]+)["'`]/);
    if (quoted?.[1]) return quoted[1].trim();

    const regex = new RegExp(`${keyword}\\s+([^.,;\\n]+)`, 'i');
    const match = String(text || '').match(regex);
    return match?.[1] ? match[1].trim() : '';
  }

  function firstMeaningfulAnchor(code) {
    const lines = String(code || '').split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
    if (!lines.length) return '';
    if (lines[0].length <= 160) return lines[0];
    return '';
  }

  function firstNonEmptyLine(code) {
    return String(code || '').split(/\r?\n/).map((line) => line.trim()).find(Boolean) || '';
  }

  function lastNonEmptyLine(code) {
    const lines = String(code || '').split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
    return lines[lines.length - 1] || '';
  }

  function uiTypeFromAction(action) {
    if (action === 'rewritefile') return 'rewrite';
    if (action === 'replaceblock') return 'replaceblock';
    if (action === 'replacefunction') return 'replacefunction';
    if (action === 'insertbefore') return 'before';
    if (action === 'insertafter') return 'after';
    if (action === 'newfile') return 'newfile';
    if (action === 'manifest') return 'manifest';
    return 'replace';
  }

  function collectPacketFileCandidates(context, globalCandidates) {
    const local = buildRecentFileCandidates(`${context.before}\n${context.after}`);
    return [...local, ...(globalCandidates || [])];
  }

  function buildRecentFileCandidates(text) {
    const sentences = splitSentences(text).slice(-12);
    const map = new Map();

    for (const sentence of sentences) {
      const refs = [...sentence.matchAll(FILE_RE)].map((match) => match[0]);
      for (const ref of refs) {
        const key = ref.toLowerCase();
        if (!map.has(key)) map.set(key, { file: ref, evidence: [] });
        const entry = map.get(key);
        if (!entry.evidence.includes(sentence.trim())) entry.evidence.push(sentence.trim());
      }
    }

    return [...map.values()].map((entry) => ({
      file: entry.file,
      evidence: entry.evidence.slice(0, 3),
      fromTree: false
    }));
  }

  function dedupeFileCandidates(candidates) {
    const map = new Map();

    for (const candidate of candidates || []) {
      if (!candidate?.file) continue;

      const key = String(candidate.file).toLowerCase();
      if (!map.has(key)) {
        map.set(key, { file: candidate.file, evidence: [], fromTree: Boolean(candidate.fromTree) });
      }

      const entry = map.get(key);
      for (const sentence of candidate.evidence || []) {
        if (!entry.evidence.includes(sentence)) entry.evidence.push(sentence);
      }

      if (candidate.fromTree) entry.fromTree = true;
    }

    return [...map.values()];
  }

  function splitSentences(text) {
    return String(text || '')
      .split(/\n+/)
      .flatMap((line) => line.split(/(?<=[.!?])\s+/))
      .map((item) => item.trim())
      .filter(Boolean);
  }

  function hasActionFlavor(text) {
    return /(replace|rewrite|insert|add|put|find|search|look for|locate|before|after|block|function|file)/i.test(String(text || ''));
  }

  function dedupeInto(target, items) {
    const seen = new Set(target.map((item) => item.id));

    for (const item of items || []) {
      if (!item?.id || seen.has(item.id)) continue;
      seen.add(item.id);
      target.push(item);
    }
  }

  function hashString(value) {
    let hash = 0;
    const text = String(value || '');

    for (let i = 0; i < text.length; i += 1) {
      hash = ((hash << 5) - hash) + text.charCodeAt(i);
      hash |= 0;
    }

    return Math.abs(hash).toString(36);
  }

  function disableContentScript() {
    state.disabled = true;
    clearTimeout(state.parseTimer);
    try { state.observer?.disconnect(); } catch {}
  }

  function safeSendMessage(payload) {
    if (state.disabled) return;

    try {
      chrome.runtime.sendMessage(payload, () => {
        const err = chrome.runtime.lastError;
        const msg = String(err?.message || '');

        if (msg.includes('Receiving end does not exist') || msg.includes('context invalidated')) {
          disableContentScript();
          return;
        }

        if (msg) console.debug('[ngo-content]', msg);
      });
    } catch (err) {
      const message = String(err?.message || err || '');

      if (message.includes('Extension context invalidated')) {
        disableContentScript();
        return;
      }

      console.debug('[ngo-content]', message);
    }
  }

  chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
    if (message?.type === 'NGO_MANUAL_SCAN_TEXT') {
      const text = String(message.text || '');
      const candidates = parseTextCandidates(text);
      if (candidates.length) sendActivity('medium');
      sendResponse({ ok: true, candidates });
      return true;
    }

    return false;
  });

  function sendActivity(level) {
    if (state.disabled) return;

    try {
      chrome.runtime.sendMessage({ type: 'NGO_ACTIVITY', level }, () => {
        const msg = String(chrome.runtime?.lastError?.message || '');

        if (
          msg.includes('Receiving end does not exist') ||
          msg.includes('context invalidated') ||
          msg.includes('Extension context invalidated')
        ) {
          disableContentScript();
        }
      });
    } catch {
      disableContentScript();
    }
  }
})();