const DEFAULT_SETTINGS = {
  host: '127.0.0.1',
  port: '5054',
  path: '/record',
  learnedPhrases: {},
};
const MAX_FILEPATH_CACHE = 5000;

const candidatesByTab = new Map();

const CONTEXT_EXCLUDE_DIRS = new Set(['node_modules', '.git', '.next', 'dist', 'build', 'coverage', 'logs', 'logarchive', 'archive', 'failed', 'scrapes', 'restore', 'localsecure', '.cache', 'tmp', 'temp']);
const CONTEXT_EXCLUDE_EXTS = new Set(['.png', '.jpg', '.jpeg', '.gif', '.webp', '.ico', '.svg', '.mp4', '.mov', '.mp3', '.wav', '.zip', '.sqlite', '.db', '.exe', '.dll', '.map']);

const ACTIVITY_DURATIONS = {
  low: 5000,
  medium: 2000,
  high: 2000,
};
function notifyCandidatePanels(tabId) {
  chrome.runtime.sendMessage({ type: 'NGO_CANDIDATES_CHANGED', tabId }, () => {
    void chrome.runtime.lastError;
  });
}


chrome.runtime.onInstalled.addListener(async () => {
  const current = await chrome.storage.local.get(Object.keys(DEFAULT_SETTINGS));
  const next = {};

  for (const [key, value] of Object.entries(DEFAULT_SETTINGS)) {
    if (current[key] === undefined) next[key] = value;
  }

  if (Object.keys(next).length) await chrome.storage.local.set(next);

  await chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true });

  chrome.contextMenus.removeAll(() => {
    chrome.contextMenus.create({
      id: 'ngo-scan-selection',
      title: 'Nodarama: Scan',
      contexts: ['selection']
    });
  });
});

chrome.contextMenus.onClicked.addListener(async (info, tab) => {
  if (info.menuItemId !== 'ngo-scan-selection') return;

  const tabId = tab?.id;
  const selectionText = String(info.selectionText || '').trim();

  if (typeof tabId !== 'number' || !selectionText) return;

  try {
    const existingPreScan = getTabState(tabId, tab);
    const now = Date.now();

    candidatesByTab.set(tabId, {
      ...existingPreScan,
      activity: {
        ...(existingPreScan.activity || emptyActivity()),
        mediumUntil: now + ACTIVITY_DURATIONS.medium
      }
    });

    const response = await chrome.tabs.sendMessage(tabId, {
      type: 'NGO_MANUAL_SCAN_TEXT',
      text: selectionText
    });

    if (!response?.ok) return;

    const existing = getTabState(tabId, tab);
    candidatesByTab.set(tabId, {
      ...existing,
      updatedAt: Date.now(),
      url: tab?.url || existing.url || '',
      title: tab?.title || existing.title || '',
      candidates: mergeCandidates(existing.candidates || [], response.candidates || []),
      activity: existing.activity || emptyActivity(),
    });

    notifyCandidatePanels(tabId);
  } catch {}
});

chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  (async () => {
    if (message?.type === 'NGO_ENSURE_CONTENT_SCRIPT') {
      const tabId = message.tabId;

      if (typeof tabId !== 'number') {
        sendResponse({ ok: false, error: 'missing_tab_id' });
        return;
      }

      try {
        await chrome.scripting.executeScript({
          target: { tabId },
          files: ['content.js']
        });

        sendResponse({ ok: true });
      } catch (err) {
        sendResponse({ ok: false, error: err?.message || String(err) });
      }

      return;
    }

    if (message?.type === 'NGO_UPDATE_CANDIDATES') {
      const tabId = sender.tab?.id;

      if (typeof tabId === 'number') {
        const existing = getTabState(tabId, sender.tab);
        const incoming = Array.isArray(message.candidates) ? message.candidates : [];

        candidatesByTab.set(tabId, {
          ...existing,
          updatedAt: Date.now(),
          url: sender.tab?.url || existing.url || '',
          title: sender.tab?.title || existing.title || '',
          candidates: mergeCandidates(existing.candidates || [], incoming),
          activity: existing.activity || emptyActivity(),
        });

        notifyCandidatePanels(tabId);
      }

      sendResponse({ ok: true });
      return;
    }

    if (message?.type === 'NGO_ACTIVITY') {
      const tabId = sender.tab?.id ?? message.tabId;
      const level = String(message.level || '').trim().toLowerCase();

      if (typeof tabId !== 'number' || !['low', 'medium', 'high'].includes(level)) {
        sendResponse({ ok: false, error: 'invalid_activity_payload' });
        return;
      }

      const now = Date.now();
      const existing = getTabState(tabId, sender.tab);
      const activity = { ...(existing.activity || emptyActivity()) };
      const key = `${level}Until`;

      activity[key] = Math.max(Number(activity[key] || 0), now + ACTIVITY_DURATIONS[level]);

      candidatesByTab.set(tabId, {
        ...existing,
        activity,
        updatedAt: Math.max(existing.updatedAt || 0, now),
        url: sender.tab?.url || existing.url || '',
        title: sender.tab?.title || existing.title || '',
      });

      sendResponse({ ok: true, activity });
      return;
    }

    if (message?.type === 'NGO_GET_TAB_STATE') {
      const tabId = message.tabId;
      const settings = await chrome.storage.local.get(Object.keys(DEFAULT_SETTINGS));

      sendResponse({
        ok: true,
        state: getTabState(tabId),
        settings: { ...DEFAULT_SETTINGS, ...settings },
      });
      return;
    }

    if (message?.type === 'NGO_CLEAR_TAB_CANDIDATES') {
      const tabId = message.tabId;

      if (typeof tabId !== 'number') {
        sendResponse({ ok: false, error: 'missing_tab_id' });
        return;
      }

      const existing = getTabState(tabId);
      candidatesByTab.set(tabId, {
        ...existing,
        updatedAt: Date.now(),
        candidates: [],
      });

      sendResponse({ ok: true });
      return;
    }

    if (message?.type === 'NGO_SAVE_SETTINGS') {
      const payload = {
        host: String(message.settings?.host || DEFAULT_SETTINGS.host).trim(),
        port: String(message.settings?.port || DEFAULT_SETTINGS.port).trim(),
        path: DEFAULT_SETTINGS.path,
      };

      await chrome.storage.local.set(payload);
      sendResponse({ ok: true, settings: payload });
      return;
    }

    if (message?.type === 'NGO_CLASSIFY_PHRASE') {
      const settings = await chrome.storage.local.get(['learnedPhrases']);
      const learnedPhrases = settings.learnedPhrases || {};
      const action = String(message.action || '').trim().toLowerCase();
      const phrase = normalizePhrase(String(message.phrase || ''));

      if (!action || !phrase) {
        sendResponse({ ok: false, error: 'missing_phrase_or_action' });
        return;
      }

      const bucket = new Set(Array.isArray(learnedPhrases[action]) ? learnedPhrases[action] : []);
      bucket.add(phrase);
      learnedPhrases[action] = [...bucket].sort();

      await chrome.storage.local.set({ learnedPhrases });
      sendResponse({ ok: true, learnedPhrases });
      return;
    }

    if (message?.type === 'NGO_SEND_MANIFESTS') {
      const settings = await chrome.storage.local.get(Object.keys(DEFAULT_SETTINGS));
      const endpoint = buildEndpoint({ ...DEFAULT_SETTINGS, ...settings });
      const payload = message.payload;

      try {
        const response = await fetch(endpoint, {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(payload),
        });

        const text = await response.text();
        let body;

        try {
          body = JSON.parse(text);
        } catch {
          body = text;
        }

        sendResponse({ ok: response.ok, status: response.status, endpoint, body });
      } catch (err) {
        sendResponse({ ok: false, error: err?.message || String(err), endpoint });
      }

      return;
    }

    if (message?.type === 'NGO_SAVE_PROJECT_JSON') {
      try {
        const json = JSON.stringify(message.data, null, 2);

        await chrome.storage.local.set({
          projectData: message.data,
          allFilePaths: flattenFilePaths(message.data?.roots || []),
        });
        await chrome.storage.local.remove(['projectDataJson']);

        sendResponse({ ok: true });
      } catch (err) {
        sendResponse({ ok: false, error: err?.message || String(err) });
      }

      return;
    }

    if (message?.type === 'NGO_PING_RECORDER') {
      const settings = await chrome.storage.local.get(Object.keys(DEFAULT_SETTINGS));
      const merged = { ...DEFAULT_SETTINGS, ...settings };
      const endpoint = buildEndpoint(merged);

      try {
        const response = await fetch(endpoint, {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({
            Task: 'Nodarama Go connection check',
            Action: 'diagnostic',
            FilePath: '',
            Payload: {},
            dryRun: true,
            source: 'nodarama_go_extension_ping'
          }),
        });

        sendResponse({ ok: true, reachable: true, status: response.status, endpoint });
      } catch (err) {
        sendResponse({ ok: false, reachable: false, error: err?.message || String(err), endpoint });
      }

      return;
    }

    if (message?.type === 'NGO_FETCH_PROJECT_TREE') {
      const settings = await chrome.storage.local.get(Object.keys(DEFAULT_SETTINGS));
      const merged = { ...DEFAULT_SETTINGS, ...settings };
      const endpoint = `http://${merged.host}:${merged.port}/read/tree`;

      try {
        const response = await fetch(endpoint, {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({}),
        });

        const text = await response.text();
        let body;

        try {
          body = JSON.parse(text);
        } catch {
          body = { ok: false, error: text };
        }

        if (body?.ok) {
          await chrome.storage.local.set({
            projectData: body,
            allFilePaths: flattenFilePaths(body.roots || [])
          });
        }

        sendResponse({ ok: response.ok, status: response.status, endpoint, body });
      } catch (err) {
        sendResponse({ ok: false, error: err?.message || String(err), endpoint });
      }

      return;
    }

    sendResponse({ ok: false, error: 'unknown_message_type' });
  })().catch((error) => {
    sendResponse({ ok: false, error: error?.message || String(error) });
  });

  return true;
});

function getTabState(tabId, tab = null) {
  const existing = candidatesByTab.get(tabId) || {};

  return {
    updatedAt: existing.updatedAt || 0,
    url: tab?.url || existing.url || '',
    title: tab?.title || existing.title || '',
    candidates: Array.isArray(existing.candidates) ? existing.candidates : [],
    activity: existing.activity || emptyActivity(),
  };
}

function emptyActivity() {
  return { lowUntil: 0, mediumUntil: 0, highUntil: 0 };
}

function mergeCandidates(existing, incoming) {
  const out = [];
  const seen = new Set();

  for (const item of [...(existing || []), ...(incoming || [])]) {
    if (!item?.id || seen.has(item.id)) continue;
    seen.add(item.id);
    out.push(item);
  }

  return out;
}

function buildEndpoint(settings) {
  const host = settings.host || DEFAULT_SETTINGS.host;
  const port = settings.port || DEFAULT_SETTINGS.port;

  return `http://${host}:${port}${DEFAULT_SETTINGS.path}`;
}

function normalizePhrase(value) {
  return value.toLowerCase().replace(/\s+/g, ' ').trim();
}

function isContextEligiblePath(filePath) {
  const clean = String(filePath || '').replace(/\\/g, '/').toLowerCase();
  if (!clean) return false;
  const parts = clean.split('/').filter(Boolean);
  if (parts.some(part => CONTEXT_EXCLUDE_DIRS.has(part))) return false;
  const ext = clean.includes('.') ? clean.slice(clean.lastIndexOf('.')) : '';
  if (CONTEXT_EXCLUDE_EXTS.has(ext)) return false;
  return true;
}

function flattenFilePaths(nodes) {
  const paths = [];

  function walk(nodeList) {
    for (const node of nodeList) {
      if (paths.length >= MAX_FILEPATH_CACHE) return;
      if (node.type === 'file' && isContextEligiblePath(node.path)) paths.push(node.path);
      if (node.children?.length) walk(node.children);
    }
  }

  walk(nodes || []);
  return paths;
}