'use strict';

(function (root, factory) {
  const api = factory();
  if (typeof module !== 'undefined' && module.exports) module.exports = api;
  if (root) root.SessionSearchUtils = api;
})(typeof globalThis !== 'undefined' ? globalThis : this, function () {
  const SESSION_ID_RE = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/ig;
  const SEARCH_WRAP_RE = /^[\s"'`“”‘’]+|[\s"'`“”‘’,.;:!?)}\]]+$/g;

  function stripSearchWrapping(value) {
    return String(value || '').trim().replace(SEARCH_WRAP_RE, '').trim();
  }

  function normalizeSearchText(value) {
    return String(value || '')
      .trim()
      .toLowerCase()
      .replace(/(^|[\s([{])[$/]+(?=[a-z0-9_-])/g, '$1')
      .replace(/\s+/g, ' ');
  }

  function normalizeSearchValue(value) {
    return normalizeSearchText(stripSearchWrapping(value));
  }

  function uniqueValues(values) {
    const seen = new Set();
    const out = [];
    for (const raw of values || []) {
      const value = String(raw || '').trim().toLowerCase();
      if (!value || seen.has(value)) continue;
      seen.add(value);
      out.push(value);
    }
    return out;
  }

  function safeDecodeSearchValue(value) {
    const raw = String(value || '');
    try {
      return decodeURIComponent(raw);
    } catch {
      return raw;
    }
  }

  function extractSessionIdsFromSearchValue(value) {
    const raw = String(value || '').trim();
    const unwrapped = stripSearchWrapping(raw);
    const variants = uniqueValues([raw, unwrapped, safeDecodeSearchValue(raw), safeDecodeSearchValue(unwrapped)]);
    const ids = [];
    for (const text of variants) {
      for (const match of text.matchAll(SESSION_ID_RE)) ids.push(match[0]);
      for (const match of text.matchAll(/(?:^|[?#&\s])(?:session|review)=([0-9a-f-]{36})(?=$|[&#\s"'`),.;:!?}])/ig)) {
        ids.push(match[1]);
      }
    }
    return uniqueValues(ids);
  }

  function parseSessionIdentityQuery(value) {
    const raw = String(value || '').trim();
    const unwrapped = stripSearchWrapping(raw);
    const decoded = safeDecodeSearchValue(unwrapped);
    const normalized = normalizeSearchText(unwrapped);
    const decodedNormalized = normalizeSearchText(decoded);
    const sessionIds = extractSessionIdsFromSearchValue(raw);
    const referenceNeedles = [];
    for (const id of sessionIds) {
      referenceNeedles.push(id, `session=${id}`, `#session=${id}`, `review=${id}`, `#review=${id}`);
    }
    if (decodedNormalized && decodedNormalized.includes('://')) referenceNeedles.push(decodedNormalized);
    return {
      raw,
      unwrapped,
      normalized,
      decoded,
      sessionIds,
      referenceNeedles: uniqueValues(referenceNeedles),
      looksLikeSessionIdentity: sessionIds.length > 0,
    };
  }

  function sessionIdentitySearchNeedles(query) {
    const parsed = parseSessionIdentityQuery(query);
    return uniqueValues([...parsed.sessionIds, parsed.normalized]);
  }

  function getSearchableSessionIds(session) {
    const rawIds = [
      session && session.sessionId,
      session && session.provisionalId,
      session && session.agentSessionId,
      session && session.agent_session_id,
    ];
    const agentSessionIds = session && (session.agentSessionIds || session.agent_session_ids);
    const agentSessionIdList = Array.isArray(agentSessionIds)
      ? agentSessionIds
      : String(agentSessionIds || '').split(',');
    rawIds.push(...agentSessionIdList);
    for (const agent of (session && Array.isArray(session.agentSessions) ? session.agentSessions : [])) {
      rawIds.push(agent && (agent.agentSessionId || agent.agent_session_id || agent.sessionId));
    }
    const seen = new Set();
    const ids = [];
    for (const raw of rawIds) {
      const value = String(raw || '').trim();
      if (!value || seen.has(value)) continue;
      seen.add(value);
      ids.push(value);
    }
    return ids;
  }

  function rememberRecentSessionIdentities(index, session) {
    for (const id of getSearchableSessionIds(session)) index.set(id, session);
  }

  function findSessionIdentityMatch(index, session) {
    if (!index || !session) return null;
    for (const id of getSearchableSessionIds(session)) {
      const existing = index.get(id);
      if (existing) return existing;
    }
    return null;
  }

  function createSessionIdentityIndex(sessions) {
    const index = new Map();
    for (const session of sessions || []) rememberRecentSessionIdentities(index, session);
    return index;
  }

  function isNewerTime(candidate, current) {
    const candidateMs = Date.parse(String(candidate || ''));
    if (!Number.isFinite(candidateMs)) return false;
    const currentMs = Date.parse(String(current || ''));
    return !Number.isFinite(currentMs) || candidateMs > currentMs;
  }

  function mergeRecentSessionMetadata(target, source) {
    if (!target || !source) return target;

    if (Array.isArray(source.agentSessions) && source.agentSessions.length > 0) {
      const existing = new Set((Array.isArray(target.agentSessions) ? target.agentSessions : [])
        .map(agent => agent && (agent.agentSessionId || agent.agent_session_id || agent.sessionId))
        .filter(Boolean));
      if (!Array.isArray(target.agentSessions)) target.agentSessions = [];
      for (const agent of source.agentSessions) {
        const id = agent && (agent.agentSessionId || agent.agent_session_id || agent.sessionId);
        if (!id || existing.has(id)) continue;
        target.agentSessions.push(agent);
        existing.add(id);
      }
    }

    const sourceAgent = getRecentSessionAgentType(source);
    const targetAgent = getRecentSessionAgentType(target);
    if (sourceAgent && sourceAgent !== 'shell' && (!target.agent || targetAgent === 'shell')) {
      target.agent = sourceAgent;
    }

    for (const field of [
      'agentType',
      'provider',
      'agentSessionId',
      'provisionalId',
      'cmd',
      'launchCommand',
      'cwd',
      'project',
      'projectEntry',
      'firstMessage',
      'title',
      'aiTitle',
      'displayTitle',
      'gitBranch',
      'model',
      'modelProvider',
      'timestamp',
      'version',
      'jsonlPath',
    ]) {
      if (!target[field] && source[field]) target[field] = source[field];
    }

    if (isNewerTime(source.modifiedAt, target.modifiedAt)) target.modifiedAt = source.modifiedAt;
    if (isNewerTime(source.fileModifiedAt, target.fileModifiedAt)) target.fileModifiedAt = source.fileModifiedAt;
    if (source.isEmpty === false && target.isEmpty !== false) target.isEmpty = false;
    if ((source.userMsgCount || 0) > (target.userMsgCount || 0)) target.userMsgCount = source.userMsgCount;
    if ((source.fileSize || 0) > (target.fileSize || 0)) target.fileSize = source.fileSize;
    if (source.starred) target.starred = true;
    if (source.userRenamed) {
      const renamedTitle = source.aiTitle || source.displayTitle || source.title || '';
      if (renamedTitle) {
        target.aiTitle = renamedTitle;
        target.title = renamedTitle;
        target.displayTitle = renamedTitle;
      }
      target.userRenamed = true;
    }
    return target;
  }

  function dedupeSessionCandidates(sessions) {
    const merged = [];
    const index = new Map();
    for (const raw of sessions || []) {
      const candidate = { ...raw };
      const existing = findSessionIdentityMatch(index, candidate);
      if (existing) {
        mergeRecentSessionMetadata(existing, candidate);
        continue;
      }
      merged.push(candidate);
      rememberRecentSessionIdentities(index, candidate);
    }
    return merged;
  }

  function scoreSessionIdMatch(session, query) {
    const needles = sessionIdentitySearchNeedles(query);
    if (needles.length === 0) return 0;

    let bestScore = 0;
    for (const candidate of getSearchableSessionIds(session)) {
      const normalizedCandidate = normalizeSearchValue(candidate);
      if (!normalizedCandidate) continue;
      for (const normalizedQuery of needles) {
        if (!normalizedQuery) continue;
        if (normalizedCandidate === normalizedQuery) {
          bestScore = Math.max(bestScore, 120);
        } else if (normalizedCandidate.startsWith(normalizedQuery)) {
          bestScore = Math.max(bestScore, 90);
        } else if (normalizedCandidate.includes(normalizedQuery)) {
          bestScore = Math.max(bestScore, 60);
        }
      }
    }
    return bestScore;
  }

  function sessionMatchesRecentSearchQuery(session, query, activeLabel) {
    const normalizedQuery = normalizeSearchValue(query);
    if (!normalizedQuery) return true;
    if (scoreSessionIdMatch(session, normalizedQuery) > 0) return true;

    const searchableFields = [
      session && session.firstMessage,
      session && session.aiTitle,
      session && session.title,
      session && session.project,
      session && session.gitBranch,
      activeLabel,
    ];
    return searchableFields.some((value) => normalizeSearchValue(value).includes(normalizedQuery));
  }

  function buildIdMatchResults(sessions, query) {
    return (sessions || [])
      .map((session) => {
        const score = scoreSessionIdMatch(session, query);
        if (!score) return null;
        return {
          ...session,
          _score: score,
          _matchField: 'session_id',
          _matchSnippet: getSearchableSessionIds(session).join(' '),
        };
      })
      .filter(Boolean)
      .sort((a, b) => (b._score - a._score) || (new Date(b.modifiedAt || 0) - new Date(a.modifiedAt || 0)));
  }

  function stripWorktreePath(value) {
    return value ? String(value).replace(/\/\.(?:claude|walle)\/worktrees\/[^/]+$/, '') : value;
  }

  const AGENT_FILTER_LABELS = {
    codex: 'Codex',
    claude: 'Claude Code',
    walle: 'Wall-E',
    gemini: 'Gemini CLI',
    opencode: 'OpenCode',
    cursor: 'Cursor',
    'claude-desktop': 'Claude Desktop',
    shell: 'Shell / Other',
  };

  const AGENT_FILTER_ORDER = ['codex', 'claude', 'walle', 'cursor', 'gemini', 'opencode', 'claude-desktop', 'shell'];

  function normalizeRecentAgentType(value) {
    if (!value) return '';
    const v = String(value).trim().toLowerCase().replace(/[_\s]+/g, '-');
    if (!v) return '';
    if (AGENT_FILTER_LABELS[v]) return v;
    if (v === 'claude-code' || v === 'claude-cli') return 'claude';
    if (v === 'anthropic') return 'claude';
    if (v === 'claude-desktop-session' || v === 'desktop') return 'claude-desktop';
    if (v === 'wall-e' || v === 'walle-session') return 'walle';
    if (v === 'gemini-cli') return 'gemini';
    if (v === 'open-code' || v === 'opencode-cli') return 'opencode';
    if (v === 'cursor-agent' || v === 'cursor-cli') return 'cursor';
    if (v === 'terminal' || v === 'unknown' || v === 'other') return 'shell';
    return '';
  }

  function detectRecentAgentTypeFromText(value) {
    if (!value) return '';
    const v = String(value).toLowerCase();
    if (v.includes('opencode') || v.includes('open-code')) return 'opencode';
    if (v.includes('cursor')) return 'cursor';
    if (v.includes('wall-e') || v.includes('walle')) return 'walle';
    if (v.includes('codex')) return 'codex';
    if (v.includes('gemini')) return 'gemini';
    if (v.includes('claude')) return 'claude';
    return '';
  }

  function getRecentSessionAgentType(session) {
    return getRecentSessionAgentTypes(session)[0] || 'shell';
  }

  function isClaudeDesktopSession(session) {
    const s = session || {};
    const meta = s.meta || {};
    const candidates = [
      s.agent,
      s.agentType,
      s.type,
      s.provider,
      s.projectEntry,
      s.project_entry,
      meta.agent,
      meta.agentType,
      meta.type,
      meta.provider,
      meta.projectEntry,
      meta.project_entry,
    ];
    if (s.desktopCacheOnly || meta.desktopCacheOnly) return true;
    return candidates.some(candidate => normalizeRecentAgentType(candidate) === 'claude-desktop');
  }

  function getRecentSessionAgentTypes(session) {
    const s = session || {};
    const types = [];
    const seen = new Set();
    const isDesktop = isClaudeDesktopSession(s);
    const addType = (candidate) => {
      const explicit = normalizeRecentAgentType(candidate);
      if (!explicit || seen.has(explicit)) return;
      if (isDesktop && explicit === 'claude') return;
      seen.add(explicit);
      types.push(explicit);
    };
    if (isDesktop) addType('claude-desktop');
    for (const candidate of [s.agent, s.agentType, s.provider]) {
      addType(candidate);
    }
    for (const agent of (Array.isArray(s.agentSessions) ? s.agentSessions : [])) {
      for (const candidate of [agent && agent.agent, agent && agent.agentType, agent && agent.provider]) {
        addType(candidate);
      }
    }
    if (s.meta && (s.meta.type === 'walle' || s.meta.agentType === 'walle')) addType('walle');
    if (s.type === 'walle') addType('walle');

    const detected = detectRecentAgentTypeFromText(s.cmd || (s.meta && s.meta.cmd) || s.launchCommand || '');
    addType(detected);
    if (types.length === 0) addType('shell');
    return types;
  }

  function recentAgentFilterLabel(agentType) {
    const normalized = normalizeRecentAgentType(agentType) || 'shell';
    return AGENT_FILTER_LABELS[normalized] || AGENT_FILTER_LABELS.shell;
  }

  function recentAgentFilterPriority(agentType) {
    const normalized = normalizeRecentAgentType(agentType) || 'shell';
    const idx = AGENT_FILTER_ORDER.indexOf(normalized);
    return idx >= 0 ? idx : AGENT_FILTER_ORDER.length;
  }

  function sessionPassesRecentSidebarFilters(session, filters) {
    const s = session || {};
    const f = filters || {};

    const emptyMode = f.emptyMode || (f.showEmptyOnly ? 'only' : 'exclude');
    if (emptyMode === 'only' && !s.isEmpty) return false;
    if (emptyMode === 'exclude' && s.isEmpty) return false;

    const project = stripWorktreePath(f.project || '');
    if (project && stripWorktreePath(s.project || s.cwd || '') !== project) return false;

    const agent = normalizeRecentAgentType(f.agent || '');
    if (agent && !getRecentSessionAgentTypes(s).includes(agent)) return false;
    return true;
  }

  function filterRecentSessionsForSidebar(sessions, filters) {
    return (sessions || []).filter((session) => sessionPassesRecentSidebarFilters(session, filters));
  }

  return {
    buildIdMatchResults,
    createSessionIdentityIndex,
    dedupeSessionCandidates,
    extractSessionIdsFromSearchValue,
    filterRecentSessionsForSidebar,
    findSessionIdentityMatch,
    mergeRecentSessionMetadata,
    getRecentSessionAgentType,
    getRecentSessionAgentTypes,
    getSearchableSessionIds,
    normalizeSearchValue,
    normalizeRecentAgentType,
    parseSessionIdentityQuery,
    recentAgentFilterLabel,
    recentAgentFilterPriority,
    scoreSessionIdMatch,
    sessionIdentitySearchNeedles,
    sessionPassesRecentSidebarFilters,
    sessionMatchesRecentSearchQuery,
    stripSearchWrapping,
    stripWorktreePath,
  };
});
