const COLUMNS = ['queued', 'running', 'waiting_human', 'waiting_subtasks', 'retry', 'succeeded', 'failed', 'cancelled'] as const;
const DETAIL_TABS = ['overview', 'activity', 'files', 'trace', 'browser', 'ssh'] as const;
const MOBILE_TABS = ['chat', 'overview', 'activity', 'files', 'trace', 'browser', 'ssh'] as const;
const SORT_MODES = ['updated', 'status', 'title'] as const;
const PAGES = ['spaces', 'calendar', 'pieces', 'settings', 'schedules', 'users', 'captcha', 'usage', 'help'] as const;
const SETTINGS_SECTIONS = [
  // User group
  'preferences',
  'pets',
  'notifications',
  'memory-learning',
  'a2a-delegations',
  // System group
  'branding',
  'paths-storage',
  'execution',
  'auth',
  'organizations',
  'server-tls',
  'push-notifications',
  // LLM group
  'llm-workers',
  'gateway-server',
  'llm-metrics',
  // Agent Runtime group
  'ask-subtasks',
  'context',
  'safety',
  'reflection',
  // Tools group (sub-sections)
  'tools-web',
  'tools-browser',
  'tools-media',
  'tools-external',
  // MCP & Connections
  'mcp',
  // SSH
  'ssh',
  // Legacy ids kept so old URLs still parse; redirected at runtime via
  // LEGACY_SECTION_REDIRECT (see SettingsPage).
  'browser-sessions',
  'provider',
  'workspace',
  'tools',
  'browser-settings',
  'search-filter',
  // Step 8 (2026-05-21): folded into 'gateway-server' as the Virtual
  // Keys section. Keep parseable so bookmarks land softly.
  'gateway-keys',
] as const;

export type DetailTabId = (typeof DETAIL_TABS)[number];
export type MobileTabId = (typeof MOBILE_TABS)[number];
export type SortMode = (typeof SORT_MODES)[number];
export type StatusColumn = (typeof COLUMNS)[number];
export type PageId = (typeof PAGES)[number];
export type SettingsSection = (typeof SETTINGS_SECTIONS)[number];

export const COLUMN_LIST = COLUMNS;
export const DETAIL_TAB_LIST = DETAIL_TABS;
export const MOBILE_TAB_LIST = MOBILE_TABS;
export const SORT_MODE_LIST = SORT_MODES;

export const COLUMN_LABELS: Record<string, string> = {
  queued: 'Inbox',
  running: 'Running',
  waiting_human: 'Waiting',
  waiting_subtasks: 'Subtasks',
  retry: 'Retry',
  succeeded: 'Done',
  failed: 'Failed',
  cancelled: 'Cancelled',
};

export interface UiUrlState {
  page: PageId;
  repo: string;
  status: 'all' | StatusColumn;
  search: string;
  sort: SortMode;
  /** Owner scope for the task list. 'mine' = own tasks only (default when auth is on),
   *  'others' = everyone else's tasks. */
  scope: 'mine' | 'others';
  detailTab: DetailTabId;
  mobileTab: MobileTabId;
  taskId: number | null;
  section?: SettingsSection;
  piece?: string;
  /** Source of the currently-selected piece ('builtin' | 'user-custom' | 'global-custom'). */
  pieceSource?: 'builtin' | 'user-custom' | 'global-custom';
  /** Active SideInfoPanel widget slug. Default: 'worker-status'. */
  dashboardWidget?: string;
  /** Selected help section id. Only meaningful when page === 'help'. */
  help?: string;
  /** Selected space id. Only meaningful when page === 'spaces'. */
  spaceId?: string;
  /** Selected chat (task) shown inline inside a space. Independent from `taskId`
   *  (the Tasks page selection). Only meaningful when page === 'spaces'. */
  spaceTaskId?: number;
  /** Search filter for the space chat list. Independent from `search` (the Tasks
   *  page filter). Only meaningful when page === 'spaces'. */
  spaceSearch: string;
  /** Status filter for the space chat list. Independent from `status` (the Tasks
   *  page filter). Only meaningful when page === 'spaces'. */
  spaceStatus: 'all' | StatusColumn;
  /** Sort mode for the space chat list. Independent from `sort` (the Tasks page
   *  filter). Only meaningful when page === 'spaces'. */
  spaceSort: SortMode;
  /** Owner scope for the space chat list ('mine' = own chats, 'others' = other
   *  members'). Independent from `scope` (the Tasks page filter). Only meaningful
   *  when page === 'spaces'. */
  spaceScope: 'mine' | 'others';
}

export function readUiUrlState(): UiUrlState {
  if (typeof window === 'undefined') {
    return {
      page: 'spaces',
      repo: '',
      status: 'all',
      search: '',
      sort: 'updated',
      scope: 'mine',
      detailTab: 'overview',
      mobileTab: 'chat',
      taskId: null,
      spaceSearch: '',
      spaceStatus: 'all',
      spaceSort: 'updated',
      spaceScope: 'mine',
    };
  }

  const params = new URLSearchParams(window.location.search);
  const page = params.get('page');
  const status = params.get('status');
  const sort = params.get('sort');
  const detailTab = params.get('tab');
  const mobileTab = params.get('mobileTab');
  const taskId = Number(params.get('task') ?? '');
  const section = params.get('section');
  const piece = params.get('piece');
  const pieceSourceRaw = params.get('pieceSource');
  const PIECE_SOURCES = ['builtin', 'user-custom', 'global-custom'] as const;
  const pieceSource = PIECE_SOURCES.includes(pieceSourceRaw as any)
    ? (pieceSourceRaw as UiUrlState['pieceSource'])
    : undefined;
  const dashboardWidget = params.get('dashboardWidget');
  const help = params.get('help');
  const spaceId = params.get('space');
  const spaceTaskId = Number(params.get('chat') ?? '');
  const spaceStatus = params.get('sstatus');
  const spaceSort = params.get('ssort');

  return {
    page: page && PAGES.includes(page as PageId) ? page as PageId : 'spaces',
    repo: params.get('repo') ?? '',
    status: status && (status === 'all' || COLUMNS.includes(status as StatusColumn))
      ? status as UiUrlState['status']
      : 'all',
    search: params.get('q') ?? '',
    sort: sort && SORT_MODES.includes(sort as SortMode) ? sort as SortMode : 'updated',
    scope: params.get('scope') === 'others' ? 'others' : 'mine',
    detailTab: detailTab && DETAIL_TABS.includes(detailTab as DetailTabId)
      ? detailTab as DetailTabId
      : 'overview',
    mobileTab: mobileTab && MOBILE_TABS.includes(mobileTab as MobileTabId)
      ? mobileTab as MobileTabId
      : 'chat',
    taskId: Number.isFinite(taskId) && taskId > 0 ? taskId : null,
    spaceSearch: params.get('sq') ?? '',
    spaceStatus: spaceStatus && (spaceStatus === 'all' || COLUMNS.includes(spaceStatus as StatusColumn))
      ? spaceStatus as UiUrlState['spaceStatus']
      : 'all',
    spaceSort: spaceSort && SORT_MODES.includes(spaceSort as SortMode) ? spaceSort as SortMode : 'updated',
    spaceScope: params.get('sscope') === 'others' ? 'others' : 'mine',
    section: section && SETTINGS_SECTIONS.includes(section as SettingsSection) ? section as SettingsSection : undefined,
    piece: piece || undefined,
    ...(pieceSource ? { pieceSource } : {}),
    ...(dashboardWidget ? { dashboardWidget } : {}),
    ...(help ? { help } : {}),
    ...(spaceId ? { spaceId } : {}),
    ...(Number.isFinite(spaceTaskId) && spaceTaskId > 0 ? { spaceTaskId } : {}),
  };
}

export function buildUiUrlStateSearch(state: UiUrlState): string {
  const params = new URLSearchParams();
  if (state.page !== 'spaces') params.set('page', state.page);
  if (state.repo) params.set('repo', state.repo);
  if (state.status !== 'all') params.set('status', state.status);
  if (state.search) params.set('q', state.search);
  if (state.sort !== 'updated') params.set('sort', state.sort);
  if (state.scope === 'others') params.set('scope', 'others');
  if (state.detailTab !== 'overview') params.set('tab', state.detailTab);
  if (state.mobileTab !== 'chat') params.set('mobileTab', state.mobileTab);
  if (state.taskId) params.set('task', String(state.taskId));
  if (state.section) params.set('section', state.section);
  if (state.piece) params.set('piece', state.piece);
  if (state.pieceSource) params.set('pieceSource', state.pieceSource);
  if (state.help) params.set('help', state.help);
  if (state.spaceId) params.set('space', state.spaceId);
  if (state.spaceTaskId) params.set('chat', String(state.spaceTaskId));
  if (state.spaceSearch) params.set('sq', state.spaceSearch);
  if (state.spaceStatus !== 'all') params.set('sstatus', state.spaceStatus);
  if (state.spaceSort !== 'updated') params.set('ssort', state.spaceSort);
  if (state.spaceScope === 'others') params.set('sscope', 'others');
  if (state.dashboardWidget && state.dashboardWidget !== 'worker-status') {
    params.set('dashboardWidget', state.dashboardWidget);
  }
  return params.toString();
}

/**
 * Detect a legacy `?page=tasks` / bare `?task=ID` deep link and pull out the
 * task id it targets. Pure so it can be unit-tested without a DOM.
 *
 * Legacy Tasks URLs come in two forms:
 *  - explicit `?page=tasks[&task=ID]`
 *  - bare `?task=ID` — Tasks was the *default* page, so `page` was omitted
 *    (the form service-worker / notification deep links emit). The new
 *    workspace scheme never uses a `task` param (it uses `chat`), so a lingering
 *    `task` param unambiguously marks a legacy deep link.
 *
 * Returns:
 *  - `null` when the URL is NOT a legacy tasks URL (no rewrite needed).
 *  - `{ legacyTaskId }` when it is. `legacyTaskId` is the positive task id to
 *    resolve into its owning workspace, or `null` for a bare `?page=tasks`
 *    (no specific task) and for invalid/non-positive ids.
 */
export function parseLegacyTasksUrl(
  rawSearch: string,
): { legacyTaskId: number | null } | null {
  const params = new URLSearchParams(rawSearch);
  const page = params.get('page');
  const isLegacy = page === 'tasks' || (page === null && params.has('task'));
  if (!isLegacy) return null;
  const raw = Number(params.get('task') ?? '');
  const legacyTaskId = Number.isFinite(raw) && raw > 0 ? raw : null;
  return { legacyTaskId };
}

/**
 * Normalize a legacy `?page=tasks` URL onto the workspace (spaces) model.
 * M2 removed the Tasks tab; old deep links must keep working.
 *
 * Pure so it can be unit-tested without a DOM. Operates on the raw query
 * string (without leading `?`) and the resolved owning-workspace id.
 *
 *  - `?page=tasks`                  → `` (bare = personal workspace, default page)
 *  - `?page=tasks&task=ID&tab=...`  → `space=<resolved>&chat=ID` (open that chat)
 *
 * The legacy `task`/`tab`/`tasksTab` keys are dropped; `task=ID` is mapped to
 * the space-scoped `chat=ID`. `resolvedSpaceId` is the workspace that actually
 * owns the deep-linked task (M3): the caller resolves it via a single-task
 * fetch and falls back to the personal workspace for spaceless rows. For a bare
 * `?page=tasks` (no task) the caller passes the personal workspace id.
 *
 * Returns the normalized query string, or `null` when no rewrite is needed
 * (not a legacy tasks URL).
 */
export function normalizeLegacyTasksUrl(
  rawSearch: string,
  resolvedSpaceId: string | null,
): string | null {
  const parsed = parseLegacyTasksUrl(rawSearch);
  if (parsed == null) return null;

  const params = new URLSearchParams(rawSearch);
  // Drop the legacy Tasks-page keys; the workspace model owns its own.
  params.delete('page');
  params.delete('task');
  params.delete('tab');
  params.delete('tasksTab');

  // Land on the task's owning workspace. If it hasn't resolved yet (null),
  // leave the rail to auto-open the personal workspace.
  if (resolvedSpaceId) params.set('space', resolvedSpaceId);

  // A specific task deep-link opens as the space chat.
  if (parsed.legacyTaskId != null) {
    params.set('chat', String(parsed.legacyTaskId));
  }

  return params.toString();
}

export function paramsEqualExcept(
  left: URLSearchParams,
  right: URLSearchParams,
  ignoredKeys: string[],
): boolean {
  const ignored = new Set(ignoredKeys);
  const normalize = (params: URLSearchParams) =>
    Array.from(params.entries())
      .filter(([key]) => !ignored.has(key))
      .sort(([leftKey, leftValue], [rightKey, rightValue]) =>
        leftKey.localeCompare(rightKey) || leftValue.localeCompare(rightValue),
      );
  return JSON.stringify(normalize(left)) === JSON.stringify(normalize(right));
}
