const DEFAULT_MODEL = 'gpt-5.2';
const STORAGE_KEY_LOGS = 'epwpScannerLogs';
const MAX_LOG_ENTRIES = 250;
const DEFAULT_TIMEOUT_SECONDS = 25;
const DEFAULT_RETRY_ATTEMPTS = 1;
const BULK_JOB_MAX_RESULTS = 300;
const BULK_JOB_NAV_TIMEOUT_MS = 65000;
const EVENT_EXTRACTION_SCHEMA = {
  type: 'object',
  additionalProperties: false,
  required: [
    'title',
    'description',
    'excerpt',
    'meta_title',
    'meta_description',
    'start',
    'end',
    'timezone',
    'venue',
    'address',
    'city',
    'country',
    'event_type',
    'event_mode',
    'event_price_range',
    'event_tags',
    'external_url',
    'contact_name',
    'contact_email',
    'contact_phone',
    'capacity',
    'price',
    'ticket_sales_enabled',
    'ticket_price',
    'ticket_button_label',
    'cover_image_url',
    'gallery_urls',
    'video_urls',
    'supporting_docs_urls',
    'sessions',
    'key_speakers',
    'recurrence',
    'recurrence_until',
    'stream_url',
    'official_event',
    'custom_fields',
    'extra_fields'
  ],
  properties: {
    title: { type: 'string' },
    description: { type: 'string' },
    excerpt: { type: 'string' },
    meta_title: { type: 'string' },
    meta_description: { type: 'string' },
    start: { type: 'string' },
    end: { type: 'string' },
    timezone: { type: 'string' },
    venue: { type: 'string' },
    address: { type: 'string' },
    city: { type: 'string' },
    country: { type: 'string' },
    event_type: { type: 'string' },
    event_mode: { type: 'string' },
    event_price_range: { type: 'string' },
    event_tags: {
      type: 'array',
      items: { type: 'string' }
    },
    external_url: { type: 'string' },
    contact_name: { type: 'string' },
    contact_email: { type: 'string' },
    contact_phone: { type: 'string' },
    capacity: { type: 'string' },
    price: { type: 'string' },
    ticket_sales_enabled: {
      type: 'string',
      enum: ['0', '1']
    },
    ticket_price: { type: 'string' },
    ticket_button_label: { type: 'string' },
    cover_image_url: { type: 'string' },
    gallery_urls: {
      type: 'array',
      items: { type: 'string' }
    },
    video_urls: {
      type: 'array',
      items: { type: 'string' }
    },
    supporting_docs_urls: {
      type: 'array',
      items: { type: 'string' }
    },
    sessions: {
      type: 'array',
      items: {
        type: 'object',
        additionalProperties: false,
        required: ['title', 'start', 'end', 'location', 'speaker'],
        properties: {
          title: { type: 'string' },
          start: { type: 'string' },
          end: { type: 'string' },
          location: { type: 'string' },
          speaker: { type: 'string' }
        }
      }
    },
    key_speakers: {
      type: 'array',
      items: {
        type: 'object',
        additionalProperties: false,
        required: ['name', 'profession', 'subject', 'image', 'details'],
        properties: {
          name: { type: 'string' },
          profession: { type: 'string' },
          subject: { type: 'string' },
          image: { type: 'string' },
          details: { type: 'string' }
        }
      }
    },
    recurrence: { type: 'string' },
    recurrence_until: { type: 'string' },
    stream_url: { type: 'string' },
    official_event: {
      type: 'string',
      enum: ['0', '1']
    },
    custom_fields: {
      type: 'object',
      additionalProperties: {
        type: 'string'
      }
    },
    extra_fields: {
      type: 'object',
      additionalProperties: {
        type: 'string'
      }
    }
  }
};

void ensureSidePanelBehavior();

const bulkJobState = {
  running: false,
  stopRequested: false,
  startedAt: '',
  finishedAt: '',
  total: 0,
  processed: 0,
  success: 0,
  failed: 0,
  currentUrl: '',
  results: [],
  settings: {}
};

chrome.runtime.onInstalled.addListener(async () => {
  await ensureSidePanelBehavior();
  await appendLog('info', 'system.install', 'Extension installed/updated.');
});

chrome.runtime.onStartup?.addListener(async () => {
  await ensureSidePanelBehavior();
});

chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
  (async () => {
    const type = String(message?.type || '').trim();
    switch (type) {
      case 'PING_PLUGIN': {
        const result = await pingPlugin(message.settings || {});
        sendResponse(result);
        return;
      }
      case 'SCAN_PAGE': {
        const result = await scanCurrentPage(message.settings || {});
        sendResponse(result);
        return;
      }
      case 'IMPORT_EVENT': {
        const result = await importEventToPlugin(
          message.settings || {},
          message.payload || {},
          message.importOptions || {},
          message.importContext || {}
        );
        sendResponse(result);
        return;
      }
      case 'START_BULK_IMPORT': {
        const result = await startBulkImport(
          message.settings || {},
          Array.isArray(message.urls) ? message.urls : []
        );
        sendResponse(result);
        return;
      }
      case 'GET_BULK_STATUS': {
        sendResponse({ ok: true, state: getBulkPublicState() });
        return;
      }
      case 'STOP_BULK_IMPORT': {
        const result = await stopBulkImport();
        sendResponse(result);
        return;
      }
      case 'SYNC_PLUGIN_PREFS': {
        const result = await syncPluginPreferences(message.settings || {}, message.preferences || {});
        sendResponse(result);
        return;
      }
      case 'TEST_OPENAI': {
        const result = await testOpenAISettings(message.settings || {});
        sendResponse(result);
        return;
      }
      case 'GET_LOGS': {
        const logs = await getLogs();
        sendResponse({ ok: true, logs });
        return;
      }
      case 'CLEAR_LOGS': {
        await clearLogs();
        sendResponse({ ok: true });
        return;
      }
      case 'WRITE_LOG': {
        const entry = message?.entry && typeof message.entry === 'object' ? message.entry : {};
        await appendLog(entry.level || 'info', entry.scope || 'manual', entry.message || '', entry.meta || {});
        sendResponse({ ok: true });
        return;
      }
      default:
        await appendLog('warn', 'runtime.message', `Unsupported action: ${type || 'unknown'}`);
        sendResponse({ ok: false, error: 'Unsupported action.' });
    }
  })().catch(async (error) => {
    const messageText = error?.message || 'Unexpected extension error.';
    await appendLog('error', 'runtime.message', messageText);
    sendResponse({ ok: false, error: messageText });
  });

  return true;
});

async function ensureSidePanelBehavior() {
  if (chrome.sidePanel && chrome.sidePanel.setPanelBehavior) {
    try {
      await chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true });
    } catch (error) {
      console.warn('Could not configure side panel behavior:', error);
    }
  }
}

async function pingPlugin(settings) {
  const body = {
    client: 'event-plugin-wp-scanner',
    version: '1.2.0'
  };
  const response = await postToPlugin('/wp-json/eep/v1/extension/ping', body, settings);
  if (!response.ok) {
    await appendLog('error', 'plugin.ping', response.error || 'Ping failed.');
  } else {
    await appendLog('info', 'plugin.ping', 'Plugin handshake successful.');
  }
  return response;
}

async function testOpenAISettings(settings) {
  const apiKey = String(settings?.openaiApiKey || '').trim();
  const model = String(settings?.openaiModel || DEFAULT_MODEL).trim() || DEFAULT_MODEL;
  if (!apiKey || !/^sk-[A-Za-z0-9]/.test(apiKey)) {
    return { ok: false, error: 'OpenAI API key is missing or invalid.' };
  }

  const payload = {
    model,
    input: [
      {
        role: 'user',
        content: [{ type: 'input_text', text: 'Reply with the single word: ok' }]
      }
    ],
    max_output_tokens: 24
  };

  const response = await fetchWithTimeout('https://api.openai.com/v1/responses', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      Authorization: `Bearer ${apiKey}`
    },
    body: JSON.stringify(payload)
  }, getTimeoutSeconds(settings));

  if (!response.ok) {
    const body = await safeResponseText(response);
    const message = `OpenAI test failed (${response.status}): ${String(body).slice(0, 240)}`;
    await appendLog('error', 'openai.test', message);
    return { ok: false, error: message };
  }

  await appendLog('info', 'openai.test', `OpenAI test succeeded with model ${model}.`);
  return { ok: true, model };
}

async function importEventToPlugin(settings, payload, importOptions = {}, importContext = {}) {
  const tab = importContext?.tab && typeof importContext.tab === 'object'
    ? importContext.tab
    : await getActiveTab();
  const importStatus = String(settings?.importStatusDefault || '').trim();
  const mediaPolicy = String(settings?.importMediaPolicy || '').trim();
  const contentMode = String(importOptions?.contentMode || settings?.importContentMode || '').trim();
  const notifyDefault = Object.prototype.hasOwnProperty.call(importOptions, 'notifyPotentialOwner')
    ? toBool(importOptions?.notifyPotentialOwner, false)
    : toBool(settings?.notifyPotentialOwner, false);
  const notifyEmail = String(importOptions?.notifyEmail || '').trim();
  const sourceUrl = String(importContext?.sourceUrl || tab?.url || '').trim();
  const sourceTitle = String(importContext?.sourceTitle || tab?.title || '').trim();
  const body = {
    event: payload,
    source_url: sourceUrl,
    source_title: sourceTitle,
    extension_preferences: {
      import_status: ['draft', 'publish'].includes(importStatus) ? importStatus : 'draft',
      media_upload_policy: ['none', 'images', 'video', 'images_video'].includes(mediaPolicy) ? mediaPolicy : 'none',
      notify_potential_owner_default: notifyDefault,
      notify_potential_owner_email: notifyEmail,
      content_mode: ['rewrite', 'verbatim'].includes(contentMode) ? contentMode : 'rewrite'
    }
  };
  const response = await postToPlugin('/wp-json/eep/v1/extension/import', body, settings);
  if (!response.ok) {
    await appendLog('error', 'plugin.import', response.error || 'Import failed.');
  } else {
    const postId = response?.data?.data?.post_id || response?.data?.post_id || '';
    await appendLog('info', 'plugin.import', postId ? `Imported event post ${postId}.` : 'Imported event.');
  }
  return response;
}

async function syncPluginPreferences(settings, preferences) {
  const importStatus = String(preferences?.import_status || settings?.importStatusDefault || '').trim();
  const mediaPolicy = String(preferences?.media_upload_policy || settings?.importMediaPolicy || '').trim();
  const contentMode = String(preferences?.content_mode || settings?.importContentMode || '').trim();
  const body = {
    import_status: ['draft', 'publish'].includes(importStatus) ? importStatus : 'draft',
    media_upload_policy: ['none', 'images', 'video', 'images_video'].includes(mediaPolicy) ? mediaPolicy : 'none',
    notify_potential_owner_default: toBool(
      preferences?.notify_potential_owner_default,
      toBool(settings?.notifyPotentialOwner, false)
    ),
    content_mode: ['rewrite', 'verbatim'].includes(contentMode) ? contentMode : 'rewrite'
  };
  const response = await postToPlugin('/wp-json/eep/v1/extension/preferences', body, settings);
  if (!response.ok) {
    await appendLog('warn', 'plugin.preferences', response.error || 'Could not sync extension preferences.');
  } else {
    await appendLog('info', 'plugin.preferences', 'Extension preferences synced to plugin.');
  }
  return response;
}

async function scanCurrentPage(settings) {
  const tab = await getActiveTab();
  if (!tab?.id) {
    await appendLog('error', 'scan.tab', 'No active tab found for scan.');
    return { ok: false, error: 'No active tab found.' };
  }

  const result = await scanTabById(tab.id, settings, {
    translateNonEnglish: false
  });
  if (!result.ok) {
    return result;
  }
  return {
    ...result,
    tab: {
      title: tab?.title || '',
      url: tab?.url || ''
    }
  };
}

async function scanTabById(tabId, settings, options = {}) {
  let injection = null;
  try {
    const executed = await chrome.scripting.executeScript({
      target: { tabId },
      func: collectPageData
    });
    injection = Array.isArray(executed) ? executed[0] : null;
  } catch (error) {
    const message = error?.message || 'Cannot scan this page (Chrome policy or permissions).';
    let tabUrl = '';
    try {
      const tab = await chrome.tabs.get(tabId);
      tabUrl = String(tab?.url || '').trim();
    } catch (_tabError) {
      tabUrl = '';
    }
    await appendLog('error', 'scan.inject', message, { tab_url: tabUrl });
    return { ok: false, error: message };
  }

  const tab = await chrome.tabs.get(tabId).catch(() => ({ id: tabId, title: '', url: '' }));
  const pageData = injection?.result || {};
  const heuristics = normalizeHeuristicEvent(pageData, tab);
  let aiEvent = {};
  let aiRaw = '';
  const aiForced = toBool(options?.forceAiExtraction, false) || toBool(options?.translateNonEnglish, false);
  const aiEnabled =
    (toBool(settings?.useAiByDefault, true) || aiForced) &&
    Boolean(String(settings?.openaiApiKey || '').trim());

  if (aiEnabled) {
    try {
      const aiResult = await extractEventWithAI(
        settings.openaiApiKey,
        settings.openaiModel || DEFAULT_MODEL,
        pageData,
        tab,
        settings,
        options
      );
      aiEvent = aiResult.event || {};
      aiRaw = aiResult.rawText || '';
      await appendLog('info', 'scan.ai', 'AI extraction completed.');
    } catch (error) {
      aiRaw = `AI extraction failed: ${error?.message || 'Unknown error'}`;
      await appendLog('warn', 'scan.ai', aiRaw);
    }
  } else {
    await appendLog('info', 'scan.ai', 'AI extraction skipped (disabled or missing key).');
  }

  const merged = mergeEventData(heuristics, aiEvent, tab);
  await appendLog('info', 'scan.page', 'Page scan completed.', {
    tab_url: tab?.url || '',
    title: merged?.title || ''
  });
  return {
    ok: true,
    tab: { title: tab?.title || '', url: tab?.url || '' },
    extracted: merged,
    diagnostics: {
      ai_used: aiEnabled,
      ai_raw: aiRaw
    }
  };
}

async function extractEventWithAI(apiKey, model, pageData, tab, settings = {}, options = {}) {
  const shouldTranslate = toBool(options?.translateNonEnglish, false);
  const customFieldSchema = normalizeCustomFieldSchemaForAI(settings?.customFieldSchema);
  const formFieldSchema = normalizeFormFieldSchemaForAI(settings?.formFieldSchema);
  const instructions = [
    'You extract structured event data from webpages for WordPress imports.',
    'Use only evidence from the provided page data.',
    'If unknown, return empty strings and empty arrays (do not invent details).',
    'Use start/end format: YYYY-MM-DD HH:mm when possible.',
    'Set official_event to "1" only when clearly official; otherwise "0".',
    'Keep description concise and professional.',
    'Fill meta_title (<=60 chars ideal) and meta_description (<=158 chars ideal) when source content allows.',
    'For tickets, use ticket_sales_enabled="1" only if ticketing is clearly available. Otherwise use "0".',
    customFieldSchema.length
      ? 'Populate custom_fields as a key/value object using only keys listed in custom_field_schema.'
      : 'If custom_field_schema is empty, return custom_fields as an empty object.',
    formFieldSchema.length
      ? 'Populate extra_fields as a key/value object using only keys listed in form_field_schema.'
      : 'If form_field_schema is empty, return extra_fields as an empty object.',
    shouldTranslate
      ? 'If the source appears to be non-English, translate all textual output fields to clear natural English while preserving factual meaning.'
      : 'Keep output language aligned with the source content unless obvious.'
  ].join('\n');

  const userPrompt = {
    page_url: tab?.url || '',
    page_title: tab?.title || '',
    source_html_lang: String(pageData?.htmlLang || '').trim(),
    translate_non_english: shouldTranslate,
    custom_field_schema: customFieldSchema,
    form_field_schema: formFieldSchema,
    extracted_page_data: pageData
  };

  const requestWithMode = async (mode = 'schema') => {
    const body = {
      model: model || DEFAULT_MODEL,
      reasoning: { effort: 'low' },
      instructions,
      input: [
        {
          role: 'user',
          content: [{ type: 'input_text', text: JSON.stringify(userPrompt) }]
        }
      ],
      max_output_tokens: 1800
    };

    if (mode === 'schema') {
      body.text = {
        format: {
          type: 'json_schema',
          name: 'event_extraction',
          strict: true,
          schema: EVENT_EXTRACTION_SCHEMA
        }
      };
    } else {
      body.text = {
        format: {
          type: 'json_object'
        }
      };
    }

    return fetchWithTimeout(
      'https://api.openai.com/v1/responses',
      {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          Authorization: `Bearer ${apiKey}`
        },
        body: JSON.stringify(body)
      },
      getTimeoutSeconds(settings)
    );
  };

  let response = await requestWithMode('schema');
  if (!response.ok) {
    const schemaError = await safeResponseText(response);
    const mayNeedFallback = response.status === 400 || response.status === 404 || response.status === 422;
    if (mayNeedFallback) {
      await appendLog('warn', 'openai.schema', `Schema mode failed (${response.status}), falling back to json_object.`);
      response = await requestWithMode('json');
    } else {
      throw new Error(`OpenAI API failed (${response.status}): ${schemaError.slice(0, 240)}`);
    }
  }

  if (!response.ok) {
    const errorText = await safeResponseText(response);
    throw new Error(`OpenAI API failed (${response.status}): ${errorText.slice(0, 240)}`);
  }

  const json = await response.json();
  const rawText = extractOpenAIText(json);
  const parsed = safeParseJson(rawText);
  if (!parsed || typeof parsed !== 'object') {
    throw new Error('AI did not return valid JSON output.');
  }

  return {
    rawText,
    event: normalizeEventObject(parsed)
  };
}

function extractOpenAIText(responseJson) {
  if (typeof responseJson?.output_text === 'string' && responseJson.output_text.trim() !== '') {
    return responseJson.output_text.trim();
  }

  const output = Array.isArray(responseJson?.output) ? responseJson.output : [];
  const chunks = [];
  for (const item of output) {
    const content = Array.isArray(item?.content) ? item.content : [];
    for (const part of content) {
      if (typeof part?.text === 'string') {
        chunks.push(part.text);
      } else if (typeof part?.output_text === 'string') {
        chunks.push(part.output_text);
      }
    }
  }

  const joined = chunks.join('\n').trim();
  if (joined) {
    return joined;
  }

  return JSON.stringify(responseJson || {});
}

function safeParseJson(text) {
  if (typeof text !== 'string' || !text.trim()) {
    return null;
  }
  try {
    return JSON.parse(text);
  } catch (error) {
    const match = text.match(/\{[\s\S]*\}/);
    if (!match) {
      return null;
    }
    try {
      return JSON.parse(match[0]);
    } catch (nestedError) {
      return null;
    }
  }
}

function normalizeEventObject(value) {
  const obj = value && typeof value === 'object' ? value : {};
  const asArray = (input) => {
    if (Array.isArray(input)) {
      return input.map((item) => String(item || '').trim()).filter(Boolean);
    }
    if (typeof input === 'string') {
      return input.split(/[\n,|]/).map((item) => item.trim()).filter(Boolean);
    }
    return [];
  };

  const sessionsInput = Array.isArray(obj.sessions) ? obj.sessions : [];
  const sessions = sessionsInput
    .map((session) => {
      if (!session || typeof session !== 'object') {
        return null;
      }
      return {
        title: String(session.title || '').trim(),
        start: String(session.start || '').trim(),
        end: String(session.end || '').trim(),
        location: String(session.location || '').trim(),
        speaker: String(session.speaker || '').trim()
      };
    })
    .filter(Boolean);

  const keySpeakersInput = Array.isArray(obj.key_speakers)
    ? obj.key_speakers
    : (Array.isArray(obj.speakers) ? obj.speakers : []);
  const keySpeakers = keySpeakersInput
    .map((speaker) => {
      if (!speaker || typeof speaker !== 'object') {
        return null;
      }
      return {
        name: String(speaker.name || '').trim(),
        profession: String(speaker.profession || speaker.professional || '').trim(),
        subject: String(speaker.subject || '').trim(),
        image: String(speaker.image || speaker.image_url || '').trim(),
        details: String(speaker.details || '').trim()
      };
    })
    .filter(Boolean);

  const customFieldsInput = obj.custom_fields && typeof obj.custom_fields === 'object'
    ? obj.custom_fields
    : (obj.custom_field_values && typeof obj.custom_field_values === 'object' ? obj.custom_field_values : {});
  const customFields = {};
  Object.entries(customFieldsInput).forEach(([key, value]) => {
    const cleanKey = sanitizeCustomFieldKey(key);
    if (!cleanKey) {
      return;
    }
    customFields[cleanKey] = String(value ?? '').trim();
  });

  const extraFieldsInput = obj.extra_fields && typeof obj.extra_fields === 'object'
    ? obj.extra_fields
    : {};
  const extraFields = {};
  Object.entries(extraFieldsInput).forEach(([key, value]) => {
    const cleanKey = sanitizeExtraFieldKey(key);
    if (!cleanKey) {
      return;
    }
    if (Array.isArray(value)) {
      const rows = value.map((item) => String(item || '').trim()).filter(Boolean);
      extraFields[cleanKey] = rows.join(', ');
      return;
    }
    extraFields[cleanKey] = String(value ?? '').trim();
  });

  return {
    title: String(obj.title || '').trim(),
    description: String(obj.description || '').trim(),
    excerpt: String(obj.excerpt || '').trim(),
    meta_title: String(obj.meta_title || obj.seo_title || '').trim(),
    meta_description: String(obj.meta_description || obj.seo_description || '').trim(),
    start: String(obj.start || '').trim(),
    end: String(obj.end || '').trim(),
    timezone: String(obj.timezone || '').trim(),
    venue: String(obj.venue || '').trim(),
    address: String(obj.address || '').trim(),
    city: String(obj.city || '').trim(),
    country: String(obj.country || '').trim(),
    event_type: String(obj.event_type || '').trim(),
    event_mode: String(obj.event_mode || '').trim(),
    event_price_range: String(obj.event_price_range || '').trim(),
    event_tags: sanitizeTagArray(asArray(obj.event_tags)),
    external_url: String(obj.external_url || '').trim(),
    contact_name: String(obj.contact_name || '').trim(),
    contact_email: String(obj.contact_email || '').trim(),
    contact_phone: String(obj.contact_phone || '').trim(),
    capacity: String(obj.capacity || '').trim(),
    price: String(obj.price || '').trim(),
    ticket_sales_enabled: String(obj.ticket_sales_enabled || obj.eep_ticket_sales_enabled || '0').trim() === '1' ? '1' : '0',
    ticket_price: String(obj.ticket_price || obj.eep_ticket_price || '').trim(),
    ticket_button_label: String(obj.ticket_button_label || obj.eep_ticket_button_label || '').trim(),
    cover_image_url: String(obj.cover_image_url || '').trim(),
    gallery_urls: asArray(obj.gallery_urls),
    video_urls: asArray(obj.video_urls),
    supporting_docs_urls: asArray(obj.supporting_docs_urls || obj.supporting_docs || obj.documents),
    sessions,
    key_speakers: keySpeakers,
    recurrence: String(obj.recurrence || '').trim(),
    recurrence_until: String(obj.recurrence_until || '').trim(),
    stream_url: String(obj.stream_url || obj.video_stream_url || '').trim(),
    official_event: String(obj.official_event || '0').trim() === '1' ? '1' : '0',
    custom_fields: customFields,
    extra_fields: extraFields
  };
}

function sanitizeTagArray(tags) {
  const input = Array.isArray(tags) ? tags : [];
  const out = [];
  input.forEach((tag) => {
    let clean = String(tag || '').trim();
    if (!clean) {
      return;
    }
    const chars = Array.from(clean);
    if (chars.length > 40) {
      clean = chars.slice(0, 40).join('');
    }
    if (!clean || out.includes(clean) || out.length >= 8) {
      return;
    }
    out.push(clean);
  });
  return out;
}

function mergeEventData(heuristics, ai, tab) {
  const aiNormalized = normalizeEventObject(ai || {});
  const heur = normalizeEventObject(heuristics || {});

  const choose = (a, b) => (a && String(a).trim() ? String(a).trim() : String(b || '').trim());
  const chooseArray = (a, b) => (Array.isArray(a) && a.length ? a : Array.isArray(b) ? b : []);

  const merged = {
    title: choose(aiNormalized.title, heur.title || tab?.title || ''),
    description: choose(aiNormalized.description, heur.description),
    excerpt: choose(aiNormalized.excerpt, heur.excerpt),
    meta_title: choose(aiNormalized.meta_title, heur.meta_title),
    meta_description: choose(aiNormalized.meta_description, heur.meta_description),
    start: choose(aiNormalized.start, heur.start),
    end: choose(aiNormalized.end, heur.end),
    timezone: choose(aiNormalized.timezone, heur.timezone || ''),
    venue: choose(aiNormalized.venue, heur.venue),
    address: choose(aiNormalized.address, heur.address),
    city: choose(aiNormalized.city, heur.city),
    country: choose(aiNormalized.country, heur.country),
    event_type: choose(aiNormalized.event_type, heur.event_type),
    event_mode: choose(aiNormalized.event_mode, heur.event_mode),
    event_price_range: choose(aiNormalized.event_price_range, heur.event_price_range),
    event_tags: sanitizeTagArray(chooseArray(aiNormalized.event_tags, heur.event_tags)),
    external_url: choose(aiNormalized.external_url, tab?.url || ''),
    contact_name: choose(aiNormalized.contact_name, heur.contact_name),
    contact_email: choose(aiNormalized.contact_email, heur.contact_email),
    contact_phone: choose(aiNormalized.contact_phone, heur.contact_phone),
    capacity: choose(aiNormalized.capacity, heur.capacity),
    price: choose(aiNormalized.price, heur.price),
    ticket_sales_enabled: aiNormalized.ticket_sales_enabled === '1' ? '1' : heur.ticket_sales_enabled === '1' ? '1' : '0',
    ticket_price: choose(aiNormalized.ticket_price, heur.ticket_price),
    ticket_button_label: choose(aiNormalized.ticket_button_label, heur.ticket_button_label),
    cover_image_url: choose(aiNormalized.cover_image_url, heur.cover_image_url),
    gallery_urls: chooseArray(aiNormalized.gallery_urls, heur.gallery_urls),
    video_urls: chooseArray(aiNormalized.video_urls, heur.video_urls),
    supporting_docs_urls: chooseArray(aiNormalized.supporting_docs_urls, heur.supporting_docs_urls),
    sessions: Array.isArray(aiNormalized.sessions) && aiNormalized.sessions.length ? aiNormalized.sessions : heur.sessions || [],
    key_speakers: Array.isArray(aiNormalized.key_speakers) && aiNormalized.key_speakers.length ? aiNormalized.key_speakers : heur.key_speakers || [],
    recurrence: choose(aiNormalized.recurrence, heur.recurrence),
    recurrence_until: choose(aiNormalized.recurrence_until, heur.recurrence_until),
    stream_url: choose(aiNormalized.stream_url, heur.stream_url),
    official_event: aiNormalized.official_event === '1' ? '1' : heur.official_event === '1' ? '1' : '0',
    custom_fields: {
      ...(heur.custom_fields && typeof heur.custom_fields === 'object' ? heur.custom_fields : {}),
      ...(aiNormalized.custom_fields && typeof aiNormalized.custom_fields === 'object' ? aiNormalized.custom_fields : {})
    },
    extra_fields: {
      ...(heur.extra_fields && typeof heur.extra_fields === 'object' ? heur.extra_fields : {}),
      ...(aiNormalized.extra_fields && typeof aiNormalized.extra_fields === 'object' ? aiNormalized.extra_fields : {})
    }
  };

  if (!merged.excerpt && merged.description) {
    merged.excerpt = merged.description.slice(0, 220).trim();
  }
  if (!merged.description && merged.excerpt) {
    merged.description = merged.excerpt;
  }
  if (!merged.meta_title && merged.title) {
    merged.meta_title = merged.title.slice(0, 60).trim();
  }
  if (!merged.meta_description) {
    merged.meta_description = (merged.excerpt || merged.description || '').slice(0, 158).trim();
  }

  return merged;
}

function normalizeHeuristicEvent(pageData, tab) {
  const ldEvent = pageData?.jsonLdEvent || {};
  const dateStart = String(ldEvent?.startDate || pageData?.detectedStartDate || '').trim();
  const dateEnd = String(ldEvent?.endDate || pageData?.detectedEndDate || '').trim();
  const dateTextCandidate = String(pageData?.dateLineCandidate || pageData?.metaDescription || '').trim();
  const locationLineCandidate = String(pageData?.locationLineCandidate || '').trim();
  const yearHintMatch = (String(tab?.title || '') + ' ' + String(pageData?.metaDescription || '') + ' ' + String(pageData?.bodySnippet || ''))
    .match(/\b(20\d{2})\b/);
  const yearHint = yearHintMatch ? parseInt(yearHintMatch[1], 10) : NaN;
  const fmtDate = (value) => {
    if (!value) {
      return '';
    }
    const d = new Date(value);
    if (Number.isNaN(d.getTime())) {
      return value.replace('T', ' ').slice(0, 16);
    }
    const pad = (n) => String(n).padStart(2, '0');
    return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
  };

  const parseDateRangeText = (rawValue, fallbackYear) => {
    const raw = String(rawValue || '').trim();
    if (!raw) {
      return { start: '', end: '' };
    }
    const monthMap = {
      jan: 1, january: 1,
      feb: 2, february: 2,
      mar: 3, march: 3,
      apr: 4, april: 4,
      may: 5,
      jun: 6, june: 6,
      jul: 7, july: 7,
      aug: 8, august: 8,
      sep: 9, sept: 9, september: 9,
      oct: 10, october: 10,
      nov: 11, november: 11,
      dec: 12, december: 12
    };
    const padded = (n) => String(n).padStart(2, '0');
    const clean = raw
      .replace(/[•·|]/g, ' ')
      .replace(/\s+/g, ' ')
      .trim();

    let month = NaN;
    let day = NaN;
    let year = NaN;

    const mdY = clean.match(/\b(jan(?:uary)?|feb(?:ruary)?|mar(?:ch)?|apr(?:il)?|may|jun(?:e)?|jul(?:y)?|aug(?:ust)?|sep(?:t|tember)?|oct(?:ober)?|nov(?:ember)?|dec(?:ember)?)\s+(\d{1,2})(?:,?\s*(\d{4}))?\b/i);
    const dMY = clean.match(/\b(\d{1,2})\s+(jan(?:uary)?|feb(?:ruary)?|mar(?:ch)?|apr(?:il)?|may|jun(?:e)?|jul(?:y)?|aug(?:ust)?|sep(?:t|tember)?|oct(?:ober)?|nov(?:ember)?|dec(?:ember)?)(?:,?\s*(\d{4}))?\b/i);
    if (mdY) {
      month = monthMap[String(mdY[1] || '').toLowerCase()];
      day = parseInt(mdY[2], 10);
      year = parseInt(mdY[3] || '', 10);
    } else if (dMY) {
      month = monthMap[String(dMY[2] || '').toLowerCase()];
      day = parseInt(dMY[1], 10);
      year = parseInt(dMY[3] || '', 10);
    }
    if (!Number.isFinite(year)) {
      year = Number.isFinite(fallbackYear) ? fallbackYear : new Date().getFullYear();
    }
    if (!Number.isFinite(month) || !Number.isFinite(day)) {
      const maybeNative = new Date(clean);
      if (!Number.isNaN(maybeNative.getTime())) {
        month = maybeNative.getMonth() + 1;
        day = maybeNative.getDate();
        year = maybeNative.getFullYear();
      }
    }
    if (!Number.isFinite(month) || !Number.isFinite(day) || !Number.isFinite(year)) {
      return { start: '', end: '' };
    }

    const times = [];
    const timeRegex = /(\d{1,2})(?::(\d{2}))?\s*(am|pm)\b/ig;
    let timeMatch = null;
    while ((timeMatch = timeRegex.exec(clean)) !== null) {
      let hh = parseInt(timeMatch[1], 10);
      const mm = parseInt(timeMatch[2] || '0', 10);
      const ap = String(timeMatch[3] || '').toLowerCase();
      if (ap === 'pm' && hh < 12) hh += 12;
      if (ap === 'am' && hh === 12) hh = 0;
      times.push({ hh, mm });
      if (times.length >= 3) break;
    }

    const startTime = times[0] || { hh: 0, mm: 0 };
    const endTime = times[1] || null;
    const start = `${year}-${padded(month)}-${padded(day)} ${padded(startTime.hh)}:${padded(startTime.mm)}`;
    let end = '';
    if (endTime) {
      end = `${year}-${padded(month)}-${padded(day)} ${padded(endTime.hh)}:${padded(endTime.mm)}`;
    }
    return { start, end };
  };

  const parseOffers = (offersValue) => {
    const rows = Array.isArray(offersValue)
      ? offersValue
      : (offersValue && typeof offersValue === 'object' ? [offersValue] : []);
    let ticketPrice = '';
    let eventPriceRange = '';
    let ticketEnabled = false;
    let externalUrl = '';
    rows.forEach((offer) => {
      if (!offer || typeof offer !== 'object') {
        return;
      }
      const offerUrl = String(offer.url || '').trim();
      if (!externalUrl && offerUrl) {
        externalUrl = offerUrl;
      }
      const availability = String(offer.availability || '').toLowerCase();
      if (availability.includes('instock') || availability.includes('presale') || availability.includes('limited') || availability.includes('online')) {
        ticketEnabled = true;
      }
      const currency = String(offer.priceCurrency || '').trim().toUpperCase();
      const directPrice = String(offer.price || '').trim();
      const lowPrice = String(offer.lowPrice || '').trim();
      const highPrice = String(offer.highPrice || '').trim();
      const currencySymbol = currency === 'GBP' ? '£' : currency === 'USD' ? '$' : currency === 'EUR' ? 'EUR ' : (currency ? `${currency} ` : '');
      if (!ticketPrice && directPrice) {
        ticketPrice = `${currencySymbol}${directPrice}`.trim();
        ticketEnabled = true;
      }
      if (!eventPriceRange && lowPrice && highPrice) {
        eventPriceRange = `${currencySymbol}${lowPrice} - ${currencySymbol}${highPrice}`.trim();
        ticketEnabled = true;
      }
      if (!eventPriceRange && directPrice) {
        eventPriceRange = `${currencySymbol}${directPrice}`.trim();
      }
    });
    return { ticketPrice, eventPriceRange, ticketEnabled, externalUrl };
  };

  const parseTicketPriceText = (value) => {
    const raw = String(value || '').replace(/\s+/g, ' ').trim();
    if (!raw) {
      return '';
    }
    if (/\bfree\b/i.test(raw)) {
      return 'Free';
    }
    const range = raw.match(/(?:[$£€]\s?\d+(?:[.,]\d{1,2})?|\d+(?:[.,]\d{1,2})?\s?(?:USD|GBP|EUR))\s*(?:-|to|–|—)\s*(?:[$£€]?\s?\d+(?:[.,]\d{1,2})?|\d+(?:[.,]\d{1,2})?\s?(?:USD|GBP|EUR))/i);
    if (range && range[0]) {
      return String(range[0]).replace(/\s+/g, ' ').trim();
    }
    const single = raw.match(/(?:[$£€]\s?\d+(?:[.,]\d{1,2})?|\d+(?:[.,]\d{1,2})?\s?(?:USD|GBP|EUR))/i);
    if (single && single[0]) {
      return String(single[0]).replace(/\s+/g, ' ').trim();
    }
    return '';
  };

  const normalizeLdLocation = (locationValue) => {
    const first = Array.isArray(locationValue) ? locationValue[0] : locationValue;
    const loc = first && typeof first === 'object' ? first : {};
    const addressObj = loc.address && typeof loc.address === 'object' ? loc.address : {};
    const addressText = typeof loc.address === 'string' ? loc.address : '';
    return {
      venue: String(loc.name || '').trim(),
      address: String(addressObj.streetAddress || addressObj.name || addressText || '').trim(),
      city: String(addressObj.addressLocality || '').trim(),
      country: String(addressObj.addressCountry || '').trim()
    };
  };

  const parseLocationLine = (lineValue) => {
    const line = String(lineValue || '').replace(/\s+/g, ' ').trim();
    if (!line) {
      return { venue: '', address: '', city: '', country: '' };
    }
    const sections = line.split(/[•·|]/).map((part) => part.trim()).filter(Boolean);
    const venue = sections[0] || '';
    const tail = sections.slice(1).join(', ').trim();
    const fallback = sections[1] || '';
    const geoBits = (tail || fallback).split(',').map((part) => part.trim()).filter(Boolean);
    const city = geoBits[0] || '';
    const country = geoBits.length > 1 ? geoBits[geoBits.length - 1] : '';
    return {
      venue,
      address: line,
      city,
      country
    };
  };

  const ldLoc = normalizeLdLocation(ldEvent?.location);
  const lineLoc = parseLocationLine(locationLineCandidate);
  const parsedRange = parseDateRangeText(dateTextCandidate, yearHint);
  const startFallback = parsedRange.start || fmtDate(pageData?.detectedStartDate || '');
  const endFallback = parsedRange.end || fmtDate(pageData?.detectedEndDate || '');
  const ldOffer = parseOffers(ldEvent?.offers || ldEvent?.offer || {});
  const ticketPriceFromText = parseTicketPriceText(pageData?.priceLineCandidate || pageData?.priceCandidates?.[0] || '');
  const ticketButtonLabelCandidate = String(pageData?.ticketButtonLabelCandidate || '').trim();
  const hasTicketLabel = /(ticket|register|book|reserve|rsvp|get tickets|buy)/i.test(ticketButtonLabelCandidate);
  const ticketSalesEnabled = ldOffer.ticketEnabled || hasTicketLabel || ticketPriceFromText !== '';
  const normalizedTicketPrice = String(ldOffer.ticketPrice || ticketPriceFromText || '').trim();
  const normalizedPriceRange = String(ldOffer.eventPriceRange || ticketPriceFromText || '').trim();

  const venue = String(ldLoc.venue || pageData?.venueCandidate || lineLoc.venue || '').trim();
  const address = String(ldLoc.address || pageData?.addressCandidate || lineLoc.address || '').trim();
  const city = String(ldLoc.city || pageData?.cityCandidate || lineLoc.city || '').trim();
  const country = String(ldLoc.country || pageData?.countryCandidate || lineLoc.country || '').trim();

  return {
    title: String(ldEvent?.name || pageData?.h1 || tab?.title || '').trim(),
    description: String(ldEvent?.description || pageData?.metaDescription || '').trim(),
    excerpt: String(pageData?.metaDescription || '').trim(),
    meta_title: String(pageData?.ogTitle || tab?.title || '').trim(),
    meta_description: String(pageData?.metaDescription || pageData?.ogDescription || '').trim(),
    start: fmtDate(dateStart) || startFallback,
    end: fmtDate(dateEnd) || endFallback,
    timezone: String(pageData?.timezoneCandidate || '').trim(),
    venue,
    address,
    city,
    country,
    event_type: '',
    event_mode: '',
    event_price_range: normalizedPriceRange,
    event_tags: [],
    external_url: String(ldOffer.externalUrl || tab?.url || '').trim(),
    contact_name: '',
    contact_email: String(pageData?.emailCandidate || '').trim(),
    contact_phone: String(pageData?.phoneCandidate || '').trim(),
    capacity: '',
    price: normalizedPriceRange,
    ticket_sales_enabled: ticketSalesEnabled ? '1' : '0',
    ticket_price: normalizedTicketPrice,
    ticket_button_label: ticketButtonLabelCandidate || (ticketSalesEnabled ? 'Get tickets' : ''),
    cover_image_url: String(ldEvent?.image || pageData?.ogImage || pageData?.images?.[0] || '').trim(),
    gallery_urls: Array.isArray(pageData?.images) ? pageData.images.slice(0, 10) : [],
    video_urls: Array.isArray(pageData?.videoUrls) ? pageData.videoUrls.slice(0, 8) : [],
    supporting_docs_urls: Array.isArray(pageData?.documentUrls) ? pageData.documentUrls.slice(0, 8) : [],
    sessions: [],
    key_speakers: [],
    recurrence: '',
    recurrence_until: '',
    stream_url: Array.isArray(pageData?.videoUrls) && pageData.videoUrls.length ? String(pageData.videoUrls[0] || '').trim() : '',
    official_event: '0',
    custom_fields: {},
    extra_fields: {}
  };
}

async function postToPlugin(endpointPath, payload, settings) {
  const siteUrl = normalizeBaseUrl(settings?.pluginBaseUrl || '');
  const appPass = String(settings?.appPass || '').trim();

  if (!siteUrl) {
    await appendLog('error', 'plugin.request', 'Plugin Site URL is required.');
    return { ok: false, error: 'Plugin Site URL is required.' };
  }
  if (!appPass) {
    await appendLog('error', 'plugin.request', 'Extension App Pass is required.');
    return { ok: false, error: 'App Pass is required.' };
  }

  const endpoint = `${siteUrl}${endpointPath}`;
  const body = JSON.stringify(payload || {});
  const timeoutSeconds = getTimeoutSeconds(settings);
  const retryAttempts = getRetryAttempts(settings);

  for (let attempt = 0; attempt <= retryAttempts; attempt += 1) {
    const timestamp = String(Math.floor(Date.now() / 1000));
    const signature = await hmacSha256Hex(appPass, `${timestamp}.${body}`);
    try {
      const response = await fetchWithTimeout(endpoint, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'X-EEP-App-Pass': appPass,
          'X-EEP-Timestamp': timestamp,
          'X-EEP-Signature': signature
        },
        body
      }, timeoutSeconds);

      const text = await safeResponseText(response);
      const json = safeParseJson(text) || {};
      if (!response.ok) {
        const message = json?.message || text || `Plugin request failed (${response.status})`;
        const err = String(message).slice(0, 500);
        await appendLog('error', 'plugin.request', err, {
          endpoint: endpointPath,
          status: response.status,
          attempt: attempt + 1
        });
        return {
          ok: false,
          status: response.status,
          error: err
        };
      }

      if (attempt > 0) {
        await appendLog('warn', 'plugin.request', `Request succeeded on retry #${attempt + 1}.`, {
          endpoint: endpointPath
        });
      }

      return {
        ok: true,
        data: json
      };
    } catch (error) {
      const message = error?.message || 'Network error contacting plugin endpoint.';
      const finalAttempt = attempt >= retryAttempts;
      await appendLog(finalAttempt ? 'error' : 'warn', 'plugin.request', message, {
        endpoint: endpointPath,
        attempt: attempt + 1
      });
      if (finalAttempt) {
        return { ok: false, error: message };
      }
      await delay(350 * (attempt + 1));
    }
  }

  return { ok: false, error: 'Plugin request failed unexpectedly.' };
}

function normalizeBaseUrl(value) {
  const raw = String(value || '').trim();
  if (!raw) {
    return '';
  }
  try {
    const url = new URL(raw.startsWith('http') ? raw : `https://${raw}`);
    return url.origin;
  } catch (error) {
    return '';
  }
}

async function hmacSha256Hex(secret, message) {
  const enc = new TextEncoder();
  const key = await crypto.subtle.importKey(
    'raw',
    enc.encode(secret),
    { name: 'HMAC', hash: 'SHA-256' },
    false,
    ['sign']
  );
  const signature = await crypto.subtle.sign('HMAC', key, enc.encode(message));
  return [...new Uint8Array(signature)].map((b) => b.toString(16).padStart(2, '0')).join('');
}

async function getActiveTab() {
  const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
  return tabs && tabs.length ? tabs[0] : null;
}

function collectPageData() {
  const toAbsoluteUrl = (value) => {
    const raw = String(value || '').trim();
    if (!raw) {
      return '';
    }
    try {
      return new URL(raw, window.location.href).toString();
    } catch (_error) {
      return '';
    }
  };

  const pickText = (selector) => {
    const node = document.querySelector(selector);
    return node ? String(node.textContent || '').trim() : '';
  };

  const getMeta = (selector, attr = 'content') => {
    const node = document.querySelector(selector);
    if (!node) {
      return '';
    }
    return String(node.getAttribute(attr) || '').trim();
  };

  const bodyRaw = String(document.body?.innerText || '');
  const bodyText = bodyRaw.replace(/\s+/g, ' ').trim();
  const bodyLines = bodyRaw
    .split(/\n+/)
    .map((line) => String(line || '').replace(/\s+/g, ' ').trim())
    .filter(Boolean);
  const emailMatch = bodyText.match(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/i);
  const phoneMatch = bodyText.match(/(?:\+?\d[\d\s().-]{7,}\d)/);
  const priceRegex = /(?:[$£€]\s?\d+(?:[.,]\d{1,2})?(?:\s*(?:-|to|–|—)\s*[$£€]?\s?\d+(?:[.,]\d{1,2})?)?|\bfree\b|\d+(?:[.,]\d{1,2})?\s?(?:USD|GBP|EUR)(?:\s*(?:-|to|–|—)\s*\d+(?:[.,]\d{1,2})?\s?(?:USD|GBP|EUR))?)/i;

  const images = Array.from(document.images || [])
    .map((img) => img.currentSrc || img.src || '')
    .filter((src) => /^https?:\/\//i.test(src))
    .slice(0, 24);

  const videoUrls = [];
  const seenVideo = new Set();
  const pushVideo = (url) => {
    const clean = toAbsoluteUrl(url);
    if (!/^https?:\/\//i.test(clean) || seenVideo.has(clean)) {
      return;
    }
    seenVideo.add(clean);
    videoUrls.push(clean);
  };

  document.querySelectorAll('iframe[src], video[src], a[href]').forEach((node) => {
    const candidate = node.getAttribute('src') || node.getAttribute('href') || '';
    if (!candidate) {
      return;
    }
    const lower = candidate.toLowerCase();
    if (lower.includes('youtube.com') || lower.includes('youtu.be') || lower.includes('vimeo.com') || lower.includes('wistia')) {
      pushVideo(candidate);
    }
  });

  const documentUrls = [];
  const seenDoc = new Set();
  const pushDoc = (url) => {
    const clean = toAbsoluteUrl(url);
    if (!/^https?:\/\//i.test(clean) || seenDoc.has(clean)) {
      return;
    }
    seenDoc.add(clean);
    documentUrls.push(clean);
  };
  document.querySelectorAll('a[href]').forEach((node) => {
    const candidate = String(node.getAttribute('href') || '').trim();
    if (!candidate) {
      return;
    }
    const lower = candidate.toLowerCase();
    if (
      lower.endsWith('.pdf')
      || lower.endsWith('.doc')
      || lower.endsWith('.docx')
      || lower.endsWith('.ppt')
      || lower.endsWith('.pptx')
      || lower.endsWith('.xls')
      || lower.endsWith('.xlsx')
      || lower.endsWith('.txt')
    ) {
      pushDoc(candidate);
    }
  });

  const pickFirstText = (selectors) => {
    const list = Array.isArray(selectors) ? selectors : [selectors];
    for (const selector of list) {
      try {
        const node = document.querySelector(selector);
        const text = node ? String(node.textContent || '').replace(/\s+/g, ' ').trim() : '';
        if (text) {
          return text;
        }
      } catch (_error) {
        // ignore invalid selectors
      }
    }
    return '';
  };

  const ticketButtons = Array.from(document.querySelectorAll('a[href], button, [role="button"]'))
    .map((node) => String(node.textContent || '').replace(/\s+/g, ' ').trim())
    .filter((text) => /(ticket|register|book|reserve|rsvp|get tickets|buy)/i.test(text))
    .slice(0, 8);

  let jsonLdEvent = {};
  const ldScripts = Array.from(document.querySelectorAll('script[type="application/ld+json"]'));
  const isEventType = (value) => {
    if (Array.isArray(value)) {
      return value.some((entry) => isEventType(entry));
    }
    const raw = String(value || '').trim().toLowerCase();
    if (!raw) {
      return false;
    }
    if (raw === 'event') {
      return true;
    }
    return raw.endsWith('event');
  };
  const findEventNode = (root) => {
    const queue = Array.isArray(root) ? [...root] : [root];
    const seen = new Set();
    while (queue.length) {
      const current = queue.shift();
      if (!current || typeof current !== 'object') {
        continue;
      }
      if (seen.has(current)) {
        continue;
      }
      seen.add(current);
      if (isEventType(current['@type'])) {
        return current;
      }
      Object.keys(current).forEach((key) => {
        const value = current[key];
        if (value && typeof value === 'object') {
          queue.push(value);
        }
      });
    }
    return null;
  };
  for (const script of ldScripts) {
    const raw = script.textContent || '';
    if (!raw.trim()) {
      continue;
    }
    try {
      const parsed = JSON.parse(raw);
      const eventNode = findEventNode(parsed);
      if (eventNode && typeof eventNode === 'object') {
        jsonLdEvent = eventNode;
        break;
      }
    } catch (error) {
      // ignore malformed JSON-LD
    }
  }

  const timeNodes = Array.from(document.querySelectorAll('time[datetime]'));
  const dateValues = timeNodes.map((node) => String(node.getAttribute('datetime') || '').trim()).filter(Boolean);
  const metaStartCandidate =
    getMeta('meta[itemprop="startDate"]') ||
    getMeta('meta[property="event:start_time"]') ||
    getMeta('meta[property="event:start"]') ||
    getMeta('meta[name="event:start_time"]');
  const metaEndCandidate =
    getMeta('meta[itemprop="endDate"]') ||
    getMeta('meta[property="event:end_time"]') ||
    getMeta('meta[property="event:end"]') ||
    getMeta('meta[name="event:end_time"]');
  const dateLineCandidate =
    pickFirstText([
      '[itemprop="startDate"]',
      '[data-testid*="date"]',
      '[data-testid*="time"]',
      '[data-testid*="datetime"]',
      '[aria-label*="date" i]',
      '[aria-label*="time" i]',
      '.event-date',
      '.event-time',
      '.event-details__data'
    ]) ||
    bodyLines.find((line) => /(jan|feb|mar|apr|may|jun|jul|aug|sep|sept|oct|nov|dec)\w*\s+\d{1,2}/i.test(line) && /(am|pm|\d{1,2}:\d{2})/i.test(line)) ||
    '';

  const locationLineCandidate =
    pickFirstText([
      '[itemprop="location"]',
      '[itemprop="address"]',
      '[data-testid*="location"]',
      '[data-testid*="venue"]',
      '[aria-label*="location" i]',
      '[aria-label*="venue" i]',
      '.event-location',
      '.venue',
      '.location'
    ]) ||
    bodyLines.find((line) => /(venue|location|address)\b/i.test(line)) ||
    bodyLines.find((line) => /[•·]/.test(line) && /,/.test(line)) ||
    '';

  const priceLineCandidate =
    pickFirstText([
      '[data-testid*="price"]',
      '[aria-label*="price" i]',
      '.event-price',
      '.ticket-price',
      '.price'
    ]) ||
    bodyLines.find((line) => priceRegex.test(line) && /(ticket|price|from|free|cost|admission|entry)/i.test(line)) ||
    bodyLines.find((line) => priceRegex.test(line)) ||
    '';

  const priceCandidates = bodyLines
    .filter((line) => priceRegex.test(line))
    .slice(0, 10);

  const venueCandidate =
    pickFirstText([
      '[itemprop="name"][itemtype*="Place"]',
      '[data-testid*="venue"]',
      '[aria-label*="venue" i]',
      '.event-venue',
      '.venue'
    ]) ||
    '';

  return {
    title: String(document.title || '').trim(),
    htmlLang: String(document.documentElement?.lang || '').trim().toLowerCase(),
    h1: pickText('h1'),
    h2: pickText('h2'),
    canonical: getMeta('link[rel="canonical"]', 'href'),
    metaDescription: getMeta('meta[name="description"]'),
    ogTitle: getMeta('meta[property="og:title"]'),
    ogDescription: getMeta('meta[property="og:description"]'),
    ogImage: getMeta('meta[property="og:image"]'),
    venueCandidate,
    addressCandidate: pickText('[itemprop="streetAddress"]') || pickText('[itemprop="address"]') || pickText('.address') || '',
    cityCandidate: pickText('[itemprop="addressLocality"]') || '',
    countryCandidate: pickText('[itemprop="addressCountry"]') || '',
    locationLineCandidate,
    dateLineCandidate,
    priceLineCandidate,
    priceCandidates,
    ticketButtonLabelCandidate: ticketButtons[0] || '',
    timezoneCandidate: Intl.DateTimeFormat().resolvedOptions().timeZone || '',
    bodySnippet: bodyText.slice(0, 10000),
    emailCandidate: emailMatch ? emailMatch[0] : '',
    phoneCandidate: phoneMatch ? phoneMatch[0] : '',
    images,
    videoUrls,
    documentUrls,
    jsonLdEvent,
    detectedStartDate: dateValues[0] || metaStartCandidate || '',
    detectedEndDate: dateValues[1] || metaEndCandidate || ''
  };
}

function getBulkPublicState() {
  return {
    running: bulkJobState.running,
    stopRequested: bulkJobState.stopRequested,
    startedAt: bulkJobState.startedAt,
    finishedAt: bulkJobState.finishedAt,
    total: bulkJobState.total,
    processed: bulkJobState.processed,
    success: bulkJobState.success,
    failed: bulkJobState.failed,
    currentUrl: bulkJobState.currentUrl,
    results: Array.isArray(bulkJobState.results) ? [...bulkJobState.results] : []
  };
}

function resetBulkJobState() {
  bulkJobState.running = false;
  bulkJobState.stopRequested = false;
  bulkJobState.startedAt = '';
  bulkJobState.finishedAt = '';
  bulkJobState.total = 0;
  bulkJobState.processed = 0;
  bulkJobState.success = 0;
  bulkJobState.failed = 0;
  bulkJobState.currentUrl = '';
  bulkJobState.results = [];
  bulkJobState.settings = {};
}

function addBulkResult(entry) {
  const safeEntry = entry && typeof entry === 'object' ? entry : {};
  bulkJobState.results.unshift({
    url: String(safeEntry.url || '').trim(),
    status: String(safeEntry.status || 'failed').trim(),
    message: String(safeEntry.message || '').trim(),
    postId: safeEntry.postId ? String(safeEntry.postId) : '',
    permalink: String(safeEntry.permalink || '').trim(),
    ts: new Date().toISOString()
  });
  if (bulkJobState.results.length > BULK_JOB_MAX_RESULTS) {
    bulkJobState.results.length = BULK_JOB_MAX_RESULTS;
  }
}

function normalizeUrlInput(url) {
  const raw = String(url || '').trim();
  if (!raw) {
    return '';
  }
  try {
    const parsed = new URL(/^https?:\/\//i.test(raw) ? raw : `https://${raw}`);
    if (!/^https?:$/i.test(parsed.protocol)) {
      return '';
    }
    parsed.hash = '';
    return parsed.toString();
  } catch (_error) {
    return '';
  }
}

async function stopBulkImport() {
  if (!bulkJobState.running) {
    return { ok: true, state: getBulkPublicState() };
  }
  bulkJobState.stopRequested = true;
  await appendLog('warn', 'bulk.stop', 'Bulk stop requested by user.');
  return { ok: true, state: getBulkPublicState() };
}

async function startBulkImport(settings, rawUrls) {
  if (bulkJobState.running) {
    return { ok: false, error: 'Bulk import is already running.', state: getBulkPublicState() };
  }
  const unique = [];
  const seen = new Set();
  for (const row of Array.isArray(rawUrls) ? rawUrls : []) {
    const normalized = normalizeUrlInput(row);
    if (!normalized || seen.has(normalized)) {
      continue;
    }
    seen.add(normalized);
    unique.push(normalized);
  }
  if (!unique.length) {
    return { ok: false, error: 'No valid URLs were provided.' };
  }

  resetBulkJobState();
  bulkJobState.running = true;
  bulkJobState.startedAt = new Date().toISOString();
  bulkJobState.total = unique.length;
  bulkJobState.settings = settings && typeof settings === 'object' ? { ...settings } : {};

  await appendLog('info', 'bulk.start', `Bulk import started for ${unique.length} URL(s).`);

  void runBulkImportQueue(unique).catch(async (error) => {
    const message = error?.message || 'Bulk import failed unexpectedly.';
    addBulkResult({ status: 'failed', message });
    bulkJobState.failed += 1;
    bulkJobState.running = false;
    bulkJobState.finishedAt = new Date().toISOString();
    bulkJobState.currentUrl = '';
    await appendLog('error', 'bulk.run', message);
  });

  return {
    ok: true,
    state: getBulkPublicState()
  };
}

async function runBulkImportQueue(urls) {
  const settings = bulkJobState.settings || {};
  const notifyEnabled = toBool(settings?.notifyPotentialOwner, false);
  const translateBulk = toBool(settings?.bulkTranslateNonEnglish, false);
  const contentMode = ['rewrite', 'verbatim'].includes(String(settings?.importContentMode || '').trim())
    ? String(settings.importContentMode).trim()
    : 'rewrite';
  const workerTab = await chrome.tabs.create({ url: 'about:blank', active: false });
  const tabId = workerTab?.id;

  if (!tabId) {
    throw new Error('Could not open worker tab for bulk import.');
  }

  try {
    for (const url of urls) {
      if (bulkJobState.stopRequested) {
        break;
      }
      bulkJobState.currentUrl = url;
      await appendLog('info', 'bulk.url', `Processing ${url}`);

      const navigated = await navigateTabAndWait(tabId, url, BULK_JOB_NAV_TIMEOUT_MS);
      if (!navigated.ok) {
        bulkJobState.processed += 1;
        bulkJobState.failed += 1;
        addBulkResult({
          url,
          status: 'failed',
          message: navigated.error || 'Navigation failed.'
        });
        continue;
      }

      const scanResult = await scanTabById(tabId, settings, {
        translateNonEnglish: translateBulk
      });
      if (!scanResult?.ok) {
        bulkJobState.processed += 1;
        bulkJobState.failed += 1;
        addBulkResult({
          url,
          status: 'failed',
          message: scanResult?.error || 'Scan failed.'
        });
        continue;
      }

      const payload = scanResult?.extracted && typeof scanResult.extracted === 'object'
        ? scanResult.extracted
        : {};
      if (!String(payload?.title || '').trim()) {
        bulkJobState.processed += 1;
        bulkJobState.failed += 1;
        addBulkResult({
          url,
          status: 'failed',
          message: 'Scan returned no listing title.'
        });
        continue;
      }

      const importOptions = {
        notifyPotentialOwner: notifyEnabled,
        notifyEmail: notifyEnabled ? String(payload?.contact_email || '').trim() : '',
        contentMode
      };
      const importResponse = await importEventToPlugin(
        settings,
        payload,
        importOptions,
        {
          sourceUrl: url,
          sourceTitle: scanResult?.tab?.title || payload?.title || '',
          tab: scanResult?.tab || {}
        }
      );
      bulkJobState.processed += 1;
      if (!importResponse?.ok) {
        bulkJobState.failed += 1;
        addBulkResult({
          url,
          status: 'failed',
          message: importResponse?.error || 'Import failed.'
        });
        continue;
      }

      const data = importResponse?.data?.data || importResponse?.data || {};
      const postId = String(data?.post_id || '').trim();
      const permalink = String(data?.permalink || '').trim();
      bulkJobState.success += 1;
      addBulkResult({
        url,
        status: 'success',
        message: postId ? `Imported as post ${postId}.` : 'Imported successfully.',
        postId,
        permalink
      });
    }
  } finally {
    try {
      await chrome.tabs.remove(tabId);
    } catch (_error) {
      // Ignore tab close failures.
    }
    if (bulkJobState.stopRequested) {
      await appendLog('warn', 'bulk.stop', 'Bulk import stopped by user.');
    } else {
      await appendLog(
        'info',
        'bulk.done',
        `Bulk import finished. Success: ${bulkJobState.success}, Failed: ${bulkJobState.failed}.`
      );
    }
    bulkJobState.running = false;
    bulkJobState.currentUrl = '';
    bulkJobState.finishedAt = new Date().toISOString();
  }
}

function navigateTabAndWait(tabId, url, timeoutMs) {
  return new Promise((resolve) => {
    let settled = false;
    let timeoutRef = 0;

    const cleanup = () => {
      chrome.tabs.onUpdated.removeListener(onUpdated);
      if (timeoutRef) {
        clearTimeout(timeoutRef);
      }
    };

    const finish = (result) => {
      if (settled) {
        return;
      }
      settled = true;
      cleanup();
      resolve(result);
    };

    const onUpdated = (updatedTabId, changeInfo, tab) => {
      if (updatedTabId !== tabId) {
        return;
      }
      if (changeInfo.status === 'complete') {
        const finalUrl = String(tab?.url || '').trim();
        if (/^chrome:\/\//i.test(finalUrl) || /^chrome-extension:\/\//i.test(finalUrl)) {
          finish({ ok: false, error: 'Cannot scan browser-internal URLs.' });
          return;
        }
        finish({ ok: true, tab: tab || {} });
      }
    };

    chrome.tabs.onUpdated.addListener(onUpdated);
    chrome.tabs.update(tabId, { url }, () => {
      if (chrome.runtime.lastError) {
        finish({ ok: false, error: chrome.runtime.lastError.message || 'Failed to navigate URL.' });
      }
    });

    timeoutRef = setTimeout(() => {
      finish({ ok: false, error: 'Navigation timed out.' });
    }, Math.max(10000, timeoutMs || BULK_JOB_NAV_TIMEOUT_MS));
  });
}

function sanitizeCustomFieldKey(value) {
  return String(value || '')
    .toLowerCase()
    .replace(/[^a-z0-9_]+/g, '_')
    .replace(/^_+|_+$/g, '')
    .slice(0, 64);
}

function sanitizeExtraFieldKey(value) {
  const key = String(value || '')
    .toLowerCase()
    .replace(/[^a-z0-9_]+/g, '_')
    .replace(/^_+|_+$/g, '')
    .slice(0, 96);
  if (!key) {
    return '';
  }
  if (key.startsWith('eep_') || key.startsWith('eep_tax_')) {
    return key;
  }
  return '';
}

function normalizeCustomFieldSchemaForAI(value) {
  const rows = Array.isArray(value) ? value : [];
  const out = [];
  const seen = new Set();
  rows.forEach((row, index) => {
    if (!row || typeof row !== 'object') {
      return;
    }
    const id = sanitizeCustomFieldKey(row.id || `custom_field_${index + 1}`);
    if (!id || seen.has(id)) {
      return;
    }
    seen.add(id);
    out.push({
      id,
      label: String(row.label || id).trim() || id,
      type: String(row.type || 'text').trim() || 'text',
      required: toBool(row.required, false),
      options: String(row.options || '').trim(),
      placeholder: String(row.placeholder || '').trim()
    });
  });
  return out;
}

function normalizeFormFieldSchemaForAI(value) {
  const rows = Array.isArray(value) ? value : [];
  const out = [];
  const seen = new Set();
  rows.forEach((row, index) => {
    if (!row || typeof row !== 'object') {
      return;
    }
    const key = sanitizeExtraFieldKey(row.key || `eep_field_${index + 1}`);
    if (!key || seen.has(key)) {
      return;
    }
    seen.add(key);
    out.push({
      key,
      label: String(row.label || key).trim() || key,
      type: String(row.type || 'text').trim() || 'text',
      required: toBool(row.required, false),
      options: String(row.options || '').trim(),
      placeholder: String(row.placeholder || '').trim()
    });
  });
  return out;
}

function toBool(value, fallback = false) {
  if (typeof value === 'boolean') {
    return value;
  }
  const normalized = String(value || '').trim().toLowerCase();
  if (value === '1' || value === 1 || normalized === 'true' || normalized === 'yes' || normalized === 'on') {
    return true;
  }
  if (value === '0' || value === 0 || normalized === 'false' || normalized === 'no' || normalized === 'off') {
    return false;
  }
  return fallback;
}

function getTimeoutSeconds(settings) {
  const parsed = Number.parseInt(String(settings?.requestTimeoutSeconds || ''), 10);
  if (Number.isNaN(parsed)) {
    return DEFAULT_TIMEOUT_SECONDS;
  }
  return Math.max(5, Math.min(120, parsed));
}

function getRetryAttempts(settings) {
  const parsed = Number.parseInt(String(settings?.retryAttempts || ''), 10);
  if (Number.isNaN(parsed)) {
    return DEFAULT_RETRY_ATTEMPTS;
  }
  return Math.max(0, Math.min(3, parsed));
}

async function fetchWithTimeout(url, options = {}, timeoutSeconds = DEFAULT_TIMEOUT_SECONDS) {
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort('timeout'), Math.max(5, timeoutSeconds) * 1000);
  try {
    const response = await fetch(url, {
      ...options,
      signal: controller.signal
    });
    return response;
  } catch (error) {
    if (error?.name === 'AbortError') {
      throw new Error(`Request timed out after ${timeoutSeconds}s.`);
    }
    throw error;
  } finally {
    clearTimeout(timer);
  }
}

async function safeResponseText(response) {
  try {
    return await response.text();
  } catch (_error) {
    return '';
  }
}

function delay(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

async function getLogs() {
  const data = await chrome.storage.local.get([STORAGE_KEY_LOGS]);
  const logs = Array.isArray(data?.[STORAGE_KEY_LOGS]) ? data[STORAGE_KEY_LOGS] : [];
  return logs;
}

async function clearLogs() {
  await chrome.storage.local.set({ [STORAGE_KEY_LOGS]: [] });
}

async function appendLog(level, scope, message, meta = {}) {
  const entry = {
    ts: new Date().toISOString(),
    level: String(level || 'info').toLowerCase(),
    scope: String(scope || 'general').slice(0, 80),
    message: String(message || '').slice(0, 1800),
    meta: meta && typeof meta === 'object' ? meta : {}
  };
  const logs = await getLogs();
  logs.unshift(entry);
  if (logs.length > MAX_LOG_ENTRIES) {
    logs.length = MAX_LOG_ENTRIES;
  }
  await chrome.storage.local.set({ [STORAGE_KEY_LOGS]: logs });
}
