/**
 * Extensions slice — global catalog caches shared across all Extensions tabs.
 * Per-tab UI state lives in useExtensionsTabState() hook, NOT here.
 */

import { api } from '@renderer/api';
import { refreshCliStatusForCurrentMode } from '@renderer/utils/refreshCliStatus';
import {
  getExtensionActionDisableReason,
  getMcpDiagnosticKey,
  getMcpOperationKey,
  getMcpProjectStateKey,
  getPluginOperationKey,
} from '@shared/utils/extensionNormalizers';
import { isProjectScopedMcpScope } from '@shared/utils/mcpScopes';

import { findPaneByTabId, updatePane } from '../utils/paneHelpers';

import type { AppState } from '../types';
import type {
  ApiKeyEntry,
  ApiKeySaveRequest,
  ApiKeyStorageStatus,
  EnrichedPlugin,
  ExtensionOperationState,
  InstalledMcpEntry,
  InstallScope,
  McpCatalogItem,
  McpCustomInstallRequest,
  McpInstallRequest,
  McpServerDiagnostic,
  PluginInstallRequest,
  SkillCatalogItem,
  SkillDeleteRequest,
  SkillDetail,
  SkillImportRequest,
  SkillReviewPreview,
  SkillUpsertRequest,
} from '@shared/types/extensions';
import type { StateCreator } from 'zustand';

// =============================================================================
// Slice Interface
// =============================================================================

export interface ExtensionsSlice {
  // ── Plugin catalog cache ──
  pluginCatalog: EnrichedPlugin[];
  pluginCatalogLoading: boolean;
  pluginCatalogError: string | null;
  pluginCatalogProjectPath: string | null;
  pluginReadmes: Record<string, string | null>;
  pluginReadmeLoading: Record<string, boolean>;

  // ── MCP catalog cache ──
  mcpBrowseCatalog: McpCatalogItem[];
  mcpBrowseNextCursor?: string;
  mcpBrowseLoading: boolean;
  mcpBrowseError: string | null;
  mcpInstalledServers: InstalledMcpEntry[];
  mcpInstalledServersByProjectPath: Record<string, InstalledMcpEntry[]>;
  mcpInstalledProjectPath: string | null;
  mcpDiagnostics: Record<string, McpServerDiagnostic>;
  mcpDiagnosticsByProjectPath: Record<string, Record<string, McpServerDiagnostic>>;
  mcpDiagnosticsLoading: boolean;
  mcpDiagnosticsLoadingByProjectPath: Record<string, boolean>;
  mcpDiagnosticsError: string | null;
  mcpDiagnosticsErrorByProjectPath: Record<string, string | null>;
  mcpDiagnosticsLastCheckedAt: number | null;
  mcpDiagnosticsLastCheckedAtByProjectPath: Record<string, number | null>;

  // ── Install progress ──
  pluginInstallProgress: Record<string, ExtensionOperationState>;
  mcpInstallProgress: Record<string, ExtensionOperationState>;
  installErrors: Record<string, string>; // keyed by scoped operation key

  // ── API Keys ──
  apiKeys: ApiKeyEntry[];
  apiKeysLoading: boolean;
  apiKeysError: string | null;
  apiKeySaving: boolean;
  apiKeyStorageStatus: ApiKeyStorageStatus | null;

  // ── Skills catalog cache ──
  skillsUserCatalog: SkillCatalogItem[];
  skillsProjectCatalogByProjectPath: Record<string, SkillCatalogItem[]>;
  skillsCatalogLoadingByProjectPath: Record<string, boolean>;
  skillsCatalogErrorByProjectPath: Record<string, string | null>;
  skillsLoading: boolean;
  skillsError: string | null;
  skillsDetailsById: Record<string, SkillDetail | null | undefined>;
  skillsDetailLoadingById: Record<string, boolean>;
  skillsDetailErrorById: Record<string, string | null>;
  skillsMutationLoading: boolean;
  skillsMutationError: string | null;

  // ── GitHub Stars (supplementary) ──
  mcpGitHubStars: Record<string, number>;

  // ── Read actions ──
  fetchPluginCatalog: (projectPath?: string, forceRefresh?: boolean) => Promise<void>;
  fetchPluginReadme: (pluginId: string) => void;
  mcpBrowse: (cursor?: string) => Promise<void>;
  mcpFetchInstalled: (projectPath?: string) => Promise<void>;
  runMcpDiagnostics: (projectPath?: string) => Promise<void>;
  fetchSkillsCatalog: (projectPath?: string) => Promise<void>;
  fetchSkillDetail: (skillId: string, projectPath?: string) => Promise<void>;
  previewSkillUpsert: (request: SkillUpsertRequest) => Promise<SkillReviewPreview>;
  applySkillUpsert: (request: SkillUpsertRequest) => Promise<SkillDetail | null>;
  previewSkillImport: (request: SkillImportRequest) => Promise<SkillReviewPreview>;
  applySkillImport: (request: SkillImportRequest) => Promise<SkillDetail | null>;
  deleteSkill: (request: SkillDeleteRequest) => Promise<void>;

  // ── Mutation actions ──
  installPlugin: (request: PluginInstallRequest) => Promise<void>;
  uninstallPlugin: (pluginId: string, scope?: InstallScope, projectPath?: string) => Promise<void>;
  installMcpServer: (request: McpInstallRequest) => Promise<void>;
  installCustomMcpServer: (request: McpCustomInstallRequest) => Promise<void>;
  uninstallMcpServer: (
    registryId: string,
    name: string,
    scope?: string,
    projectPath?: string
  ) => Promise<void>;

  // ── API Keys actions ──
  fetchApiKeys: () => Promise<void>;
  fetchApiKeyStorageStatus: () => Promise<void>;
  saveApiKey: (request: ApiKeySaveRequest) => Promise<void>;
  deleteApiKey: (id: string) => Promise<void>;

  // ── Tab opener ──
  openExtensionsTab: () => void;

  // ── GitHub Stars ──
  fetchMcpGitHubStars: (repositoryUrls: string[]) => void;
}

// =============================================================================
// Slice Creator
// =============================================================================

let pluginFetchInFlight: { key: string; promise: Promise<void>; token: symbol } | null = null;
let pluginCatalogRequestSeq = 0;
const pluginSuccessResetTimers = new Map<string, ReturnType<typeof setTimeout>>();
const mcpSuccessResetTimers = new Map<string, ReturnType<typeof setTimeout>>();
const mcpDiagnosticsInFlightByKey = new Map<string, Promise<void>>();
let skillsCatalogRequestSeq = 0;
let skillsDetailRequestSeq = 0;
const latestSkillsCatalogRequestByKey = new Map<string, number>();
const latestSkillsDetailRequestById = new Map<string, number>();

const USER_SKILLS_CATALOG_KEY = '__user__';

function hasAnyLoading(loadingMap: Record<string, boolean>): boolean {
  return Object.values(loadingMap).some(Boolean);
}

function getPluginCatalogKey(projectPath?: string): string {
  return projectPath ?? '__user__';
}

function buildPluginIdSet(catalog: EnrichedPlugin[]): Set<string> {
  return new Set(catalog.map((plugin) => plugin.pluginId));
}

function isPluginOperationKeyForPlugin(operationKey: string, pluginId: string): boolean {
  return operationKey.startsWith(`plugin:${pluginId}:`);
}

function clearPluginOperationState(
  pluginIds: Set<string>,
  pluginInstallProgress: Record<string, ExtensionOperationState>,
  installErrors: Record<string, string>
): {
  pluginInstallProgress: Record<string, ExtensionOperationState>;
  installErrors: Record<string, string>;
} {
  if (pluginIds.size === 0) {
    return { pluginInstallProgress, installErrors };
  }

  const nextPluginInstallProgress = { ...pluginInstallProgress };
  const nextInstallErrors = { ...installErrors };
  const pluginIdsList = Array.from(pluginIds);

  for (const operationKey of Object.keys(nextPluginInstallProgress)) {
    if (pluginIdsList.some((pluginId) => isPluginOperationKeyForPlugin(operationKey, pluginId))) {
      delete nextPluginInstallProgress[operationKey];
    }
  }

  for (const operationKey of Object.keys(nextInstallErrors)) {
    if (pluginIdsList.some((pluginId) => isPluginOperationKeyForPlugin(operationKey, pluginId))) {
      delete nextInstallErrors[operationKey];
    }
  }

  return {
    pluginInstallProgress: nextPluginInstallProgress,
    installErrors: nextInstallErrors,
  };
}

function clearPluginSuccessResetTimer(operationKey: string): void {
  const timer = pluginSuccessResetTimers.get(operationKey);
  if (!timer) {
    return;
  }

  clearTimeout(timer);
  pluginSuccessResetTimers.delete(operationKey);
}

function clearPluginSuccessResetTimers(pluginIds: Set<string>): void {
  const pluginIdsList = Array.from(pluginIds);
  for (const operationKey of Array.from(pluginSuccessResetTimers.keys())) {
    if (pluginIdsList.some((pluginId) => isPluginOperationKeyForPlugin(operationKey, pluginId))) {
      clearPluginSuccessResetTimer(operationKey);
    }
  }
}

function schedulePluginSuccessReset(
  operationKey: string,
  set: Parameters<StateCreator<AppState, [], [], ExtensionsSlice>>[0]
): void {
  clearPluginSuccessResetTimer(operationKey);
  const timer = setTimeout(() => {
    pluginSuccessResetTimers.delete(operationKey);
    set((prev) => {
      if (prev.pluginInstallProgress[operationKey] !== 'success') {
        return {};
      }

      return {
        pluginInstallProgress: { ...prev.pluginInstallProgress, [operationKey]: 'idle' },
      };
    });
  }, SUCCESS_DISPLAY_MS);
  pluginSuccessResetTimers.set(operationKey, timer);
}

function getCustomMcpOperationKey(
  serverName: string,
  scope: InstallScope,
  projectPath?: string | null
): string {
  if (scope === 'project' || scope === 'local') {
    return `mcp-custom:${serverName}:${scope}:${getMcpProjectStateKey(projectPath)}`;
  }
  return `mcp-custom:${serverName}:${scope}`;
}

function isProjectScopedMcpOperationKey(operationKey: string): boolean {
  return (
    operationKey.includes(':project:') ||
    operationKey.endsWith(':project') ||
    operationKey.includes(':local:') ||
    operationKey.endsWith(':local')
  );
}

function clearMcpSuccessResetTimer(operationKey: string): void {
  const timer = mcpSuccessResetTimers.get(operationKey);
  if (!timer) {
    return;
  }

  clearTimeout(timer);
  mcpSuccessResetTimers.delete(operationKey);
}

function scheduleMcpSuccessReset(
  operationKey: string,
  set: Parameters<StateCreator<AppState, [], [], ExtensionsSlice>>[0]
): void {
  clearMcpSuccessResetTimer(operationKey);
  const timer = setTimeout(() => {
    mcpSuccessResetTimers.delete(operationKey);
    set((prev) => {
      if (prev.mcpInstallProgress[operationKey] !== 'success') {
        return {};
      }

      return {
        mcpInstallProgress: { ...prev.mcpInstallProgress, [operationKey]: 'idle' },
      };
    });
  }, SUCCESS_DISPLAY_MS);
  mcpSuccessResetTimers.set(operationKey, timer);
}

function clearMcpProjectScopedOperationState(
  mcpInstallProgress: Record<string, ExtensionOperationState>,
  installErrors: Record<string, string>
): {
  mcpInstallProgress: Record<string, ExtensionOperationState>;
  installErrors: Record<string, string>;
} {
  const nextMcpInstallProgress = { ...mcpInstallProgress };
  const nextInstallErrors = { ...installErrors };

  for (const operationKey of Object.keys(nextMcpInstallProgress)) {
    if (
      (operationKey.startsWith('mcp:') || operationKey.startsWith('mcp-custom:')) &&
      isProjectScopedMcpOperationKey(operationKey)
    ) {
      delete nextMcpInstallProgress[operationKey];
    }
  }

  for (const operationKey of Object.keys(nextInstallErrors)) {
    if (
      (operationKey.startsWith('mcp:') || operationKey.startsWith('mcp-custom:')) &&
      isProjectScopedMcpOperationKey(operationKey)
    ) {
      delete nextInstallErrors[operationKey];
    }
  }

  return {
    mcpInstallProgress: nextMcpInstallProgress,
    installErrors: nextInstallErrors,
  };
}

function clearMcpProjectScopedSuccessResetTimers(): void {
  for (const operationKey of Array.from(mcpSuccessResetTimers.keys())) {
    if (isProjectScopedMcpOperationKey(operationKey)) {
      clearMcpSuccessResetTimer(operationKey);
    }
  }
}

function getSkillsCatalogKey(projectPath?: string): string {
  return projectPath ?? USER_SKILLS_CATALOG_KEY;
}

function upsertApiKeyEntry(entries: ApiKeyEntry[], entry: ApiKeyEntry): ApiKeyEntry[] {
  const nextEntries = entries.filter((candidate) => candidate.id !== entry.id);
  return [entry, ...nextEntries];
}

/** Duration to show "success" state before returning to idle */
const SUCCESS_DISPLAY_MS = 2_000;
const PROJECT_SCOPE_REQUIRED_MESSAGE =
  'Project- and local-scoped plugins require an active project in the Extensions tab.';

function refreshConfiguredCliStatus(
  state: Pick<AppState, 'appConfig' | 'bootstrapCliStatus' | 'fetchCliStatus'>
): Promise<void> {
  return refreshCliStatusForCurrentMode({
    multimodelEnabled: state.appConfig?.general?.multimodelEnabled ?? false,
    bootstrapCliStatus: state.bootstrapCliStatus,
    fetchCliStatus: state.fetchCliStatus,
  });
}

function getExtensionActionCliStatusState(
  state: Pick<AppState, 'cliStatus' | 'cliStatusLoading'>
): Pick<Parameters<typeof getExtensionActionDisableReason>[0], 'cliStatus' | 'cliStatusLoading'> {
  return {
    cliStatus: state.cliStatus,
    cliStatusLoading: state.cliStatus === null && state.cliStatusLoading,
  };
}

export const createExtensionsSlice: StateCreator<AppState, [], [], ExtensionsSlice> = (
  set,
  get
) => ({
  // ── Initial state ──
  pluginCatalog: [],
  pluginCatalogLoading: false,
  pluginCatalogError: null,
  pluginCatalogProjectPath: null,
  pluginReadmes: {},
  pluginReadmeLoading: {},

  mcpBrowseCatalog: [],
  mcpBrowseNextCursor: undefined,
  mcpBrowseLoading: false,
  mcpBrowseError: null,
  mcpInstalledServers: [],
  mcpInstalledServersByProjectPath: {},
  mcpInstalledProjectPath: null,
  mcpDiagnostics: {},
  mcpDiagnosticsByProjectPath: {},
  mcpDiagnosticsLoading: false,
  mcpDiagnosticsLoadingByProjectPath: {},
  mcpDiagnosticsError: null,
  mcpDiagnosticsErrorByProjectPath: {},
  mcpDiagnosticsLastCheckedAt: null,
  mcpDiagnosticsLastCheckedAtByProjectPath: {},

  pluginInstallProgress: {},
  mcpInstallProgress: {},
  installErrors: {},

  apiKeys: [],
  apiKeysLoading: false,
  apiKeysError: null,
  apiKeySaving: false,
  apiKeyStorageStatus: null,

  skillsUserCatalog: [],
  skillsProjectCatalogByProjectPath: {},
  skillsCatalogLoadingByProjectPath: {},
  skillsCatalogErrorByProjectPath: {},
  skillsLoading: false,
  skillsError: null,
  skillsDetailsById: {},
  skillsDetailLoadingById: {},
  skillsDetailErrorById: {},
  skillsMutationLoading: false,
  skillsMutationError: null,

  mcpGitHubStars: {},

  // ── Plugin catalog fetch ──
  fetchPluginCatalog: async (projectPath?: string, forceRefresh?: boolean) => {
    if (!api.plugins) return;
    const requestKey = getPluginCatalogKey(projectPath);

    // Dedup concurrent requests
    if (pluginFetchInFlight && !forceRefresh && pluginFetchInFlight.key === requestKey) {
      await pluginFetchInFlight.promise;
      return;
    }

    const requestSeq = ++pluginCatalogRequestSeq;
    const requestToken = Symbol('pluginCatalogRequest');
    set({ pluginCatalogLoading: true, pluginCatalogError: null });

    let currentPromise: Promise<void> | null = null;
    currentPromise = (async () => {
      try {
        const result = await api.plugins!.getAll(projectPath, forceRefresh);
        set((prev) => {
          if (requestSeq !== pluginCatalogRequestSeq) {
            return {};
          }

          const nextProjectPath = projectPath ?? null;
          const isSameProjectContext = prev.pluginCatalogProjectPath === nextProjectPath;
          const pluginIdsToClear = isSameProjectContext
            ? new Set<string>()
            : new Set([...buildPluginIdSet(prev.pluginCatalog), ...buildPluginIdSet(result)]);
          const nextOperationState = clearPluginOperationState(
            pluginIdsToClear,
            prev.pluginInstallProgress,
            prev.installErrors
          );
          clearPluginSuccessResetTimers(pluginIdsToClear);

          return {
            pluginCatalog: result,
            pluginCatalogLoading: false,
            pluginCatalogError: null,
            pluginCatalogProjectPath: nextProjectPath,
            pluginInstallProgress: nextOperationState.pluginInstallProgress,
            installErrors: nextOperationState.installErrors,
          };
        });
      } catch (err) {
        set((prev) => {
          if (requestSeq !== pluginCatalogRequestSeq) {
            return {};
          }

          const nextProjectPath = projectPath ?? null;
          const isSameProjectContext = prev.pluginCatalogProjectPath === nextProjectPath;
          const nextOperationState = clearPluginOperationState(
            isSameProjectContext ? new Set<string>() : buildPluginIdSet(prev.pluginCatalog),
            prev.pluginInstallProgress,
            prev.installErrors
          );
          clearPluginSuccessResetTimers(
            isSameProjectContext ? new Set<string>() : buildPluginIdSet(prev.pluginCatalog)
          );

          return {
            pluginCatalog: isSameProjectContext ? prev.pluginCatalog : [],
            pluginCatalogLoading: false,
            pluginCatalogError: err instanceof Error ? err.message : 'Failed to load plugins',
            pluginCatalogProjectPath: nextProjectPath,
            pluginInstallProgress: nextOperationState.pluginInstallProgress,
            installErrors: nextOperationState.installErrors,
          };
        });
      } finally {
        if (pluginFetchInFlight?.token === requestToken) {
          pluginFetchInFlight = null;
        }
      }
    })();

    pluginFetchInFlight = { key: requestKey, promise: currentPromise, token: requestToken };
    await currentPromise;
  },

  // ── Plugin README fetch ──
  fetchPluginReadme: (pluginId: string) => {
    if (!api.plugins) return;
    const state = get();
    const cachedReadme = state.pluginReadmes[pluginId];
    if (
      (cachedReadme !== undefined && cachedReadme !== null) ||
      state.pluginReadmeLoading[pluginId]
    ) {
      return;
    }

    set((prev) => ({
      pluginReadmeLoading: { ...prev.pluginReadmeLoading, [pluginId]: true },
    }));

    void api.plugins.getReadme(pluginId).then(
      (readme) => {
        set((prev) => ({
          pluginReadmes: { ...prev.pluginReadmes, [pluginId]: readme },
          pluginReadmeLoading: { ...prev.pluginReadmeLoading, [pluginId]: false },
        }));
      },
      () => {
        set((prev) => ({
          pluginReadmes: { ...prev.pluginReadmes, [pluginId]: null },
          pluginReadmeLoading: { ...prev.pluginReadmeLoading, [pluginId]: false },
        }));
      }
    );
  },

  // ── MCP browse ──
  mcpBrowse: async (cursor?: string) => {
    if (!api.mcpRegistry) return;

    set({ mcpBrowseLoading: true, mcpBrowseError: null });
    try {
      const result = await api.mcpRegistry.browse(cursor);
      set((prev) => {
        if (!cursor) {
          return {
            mcpBrowseCatalog: result.servers,
            mcpBrowseNextCursor: result.nextCursor,
            mcpBrowseLoading: false,
          };
        }
        // Deduplicate: existing IDs take precedence
        const existingIds = new Set(prev.mcpBrowseCatalog.map((s) => s.id));
        const newServers = result.servers.filter((s) => !existingIds.has(s.id));
        return {
          mcpBrowseCatalog: [...prev.mcpBrowseCatalog, ...newServers],
          mcpBrowseNextCursor: result.nextCursor,
          mcpBrowseLoading: false,
        };
      });
    } catch (err) {
      set({
        mcpBrowseLoading: false,
        mcpBrowseError: err instanceof Error ? err.message : 'Failed to browse MCP servers',
      });
    }
  },

  // ── MCP installed fetch ──
  mcpFetchInstalled: async (projectPath?: string) => {
    if (!api.mcpRegistry) return;

    try {
      const installed = await api.mcpRegistry.getInstalled(projectPath);
      set((prev) => {
        const nextProjectPath = projectPath ?? null;
        const stateKey = getMcpProjectStateKey(nextProjectPath);
        const isSameProjectContext = prev.mcpInstalledProjectPath === nextProjectPath;
        const nextOperationState = isSameProjectContext
          ? {
              mcpInstallProgress: prev.mcpInstallProgress,
              installErrors: prev.installErrors,
            }
          : clearMcpProjectScopedOperationState(prev.mcpInstallProgress, prev.installErrors);

        if (!isSameProjectContext) {
          clearMcpProjectScopedSuccessResetTimers();
        }

        return {
          mcpInstalledServers: installed,
          mcpInstalledServersByProjectPath: {
            ...prev.mcpInstalledServersByProjectPath,
            [stateKey]: installed,
          },
          mcpInstalledProjectPath: nextProjectPath,
          mcpInstallProgress: nextOperationState.mcpInstallProgress,
          installErrors: nextOperationState.installErrors,
        };
      });
    } catch {
      // Silently fail — installed state is supplementary
    }
  },

  runMcpDiagnostics: async (projectPath?: string) => {
    const mcpRegistry = api.mcpRegistry;
    if (!mcpRegistry) return;
    const projectStateKey = getMcpProjectStateKey(projectPath);

    const existing = mcpDiagnosticsInFlightByKey.get(projectStateKey);
    if (existing) {
      await existing;
      return;
    }

    set((prev) => ({
      mcpDiagnosticsLoading: true,
      mcpDiagnosticsError: null,
      mcpDiagnosticsLoadingByProjectPath: {
        ...prev.mcpDiagnosticsLoadingByProjectPath,
        [projectStateKey]: true,
      },
      mcpDiagnosticsErrorByProjectPath: {
        ...prev.mcpDiagnosticsErrorByProjectPath,
        [projectStateKey]: null,
      },
    }));

    const promise = (async () => {
      try {
        const diagnostics = await mcpRegistry.diagnose(projectPath);
        const diagnosticsRecord = Object.fromEntries(
          diagnostics.map((entry) => [getMcpDiagnosticKey(entry.name, entry.scope), entry] as const)
        );
        const checkedAt = Date.now();
        set({
          mcpDiagnostics: diagnosticsRecord,
          mcpDiagnosticsLoading: false,
          mcpDiagnosticsByProjectPath: {
            ...get().mcpDiagnosticsByProjectPath,
            [projectStateKey]: diagnosticsRecord,
          },
          mcpDiagnosticsLoadingByProjectPath: {
            ...get().mcpDiagnosticsLoadingByProjectPath,
            [projectStateKey]: false,
          },
          mcpDiagnosticsLastCheckedAt: checkedAt,
          mcpDiagnosticsLastCheckedAtByProjectPath: {
            ...get().mcpDiagnosticsLastCheckedAtByProjectPath,
            [projectStateKey]: checkedAt,
          },
        });
      } catch (err) {
        const errorMessage =
          err instanceof Error ? err.message : 'Failed to check MCP server health';
        set({
          mcpDiagnosticsLoading: false,
          mcpDiagnosticsError: errorMessage,
          mcpDiagnosticsLoadingByProjectPath: {
            ...get().mcpDiagnosticsLoadingByProjectPath,
            [projectStateKey]: false,
          },
          mcpDiagnosticsErrorByProjectPath: {
            ...get().mcpDiagnosticsErrorByProjectPath,
            [projectStateKey]: errorMessage,
          },
        });
      } finally {
        mcpDiagnosticsInFlightByKey.delete(projectStateKey);
      }
    })();

    mcpDiagnosticsInFlightByKey.set(projectStateKey, promise);
    await promise;
  },

  fetchSkillsCatalog: async (projectPath?: string) => {
    if (!api.skills) return;

    const requestKey = getSkillsCatalogKey(projectPath);
    const requestId = ++skillsCatalogRequestSeq;
    latestSkillsCatalogRequestByKey.set(requestKey, requestId);

    set((prev) => {
      const nextLoadingByProjectPath = {
        ...prev.skillsCatalogLoadingByProjectPath,
        [requestKey]: true,
      };
      return {
        skillsCatalogLoadingByProjectPath: nextLoadingByProjectPath,
        skillsCatalogErrorByProjectPath: {
          ...prev.skillsCatalogErrorByProjectPath,
          [requestKey]: null,
        },
        skillsLoading: hasAnyLoading(nextLoadingByProjectPath),
        skillsError: null,
      };
    });
    try {
      const skills = await api.skills.list(projectPath);
      if (latestSkillsCatalogRequestByKey.get(requestKey) !== requestId) {
        return;
      }

      set((prev) => ({
        skillsCatalogLoadingByProjectPath: {
          ...prev.skillsCatalogLoadingByProjectPath,
          [requestKey]: false,
        },
        skillsCatalogErrorByProjectPath: {
          ...prev.skillsCatalogErrorByProjectPath,
          [requestKey]: null,
        },
        skillsLoading: hasAnyLoading({
          ...prev.skillsCatalogLoadingByProjectPath,
          [requestKey]: false,
        }),
        skillsError: null,
        skillsUserCatalog: skills.filter((skill) => skill.scope === 'user'),
        skillsProjectCatalogByProjectPath: projectPath
          ? {
              ...prev.skillsProjectCatalogByProjectPath,
              [projectPath]: skills.filter((skill) => skill.scope === 'project'),
            }
          : prev.skillsProjectCatalogByProjectPath,
      }));
    } catch (err) {
      if (latestSkillsCatalogRequestByKey.get(requestKey) !== requestId) {
        return;
      }

      const message = err instanceof Error ? err.message : 'Failed to load skills';
      set((prev) => ({
        skillsCatalogLoadingByProjectPath: {
          ...prev.skillsCatalogLoadingByProjectPath,
          [requestKey]: false,
        },
        skillsCatalogErrorByProjectPath: {
          ...prev.skillsCatalogErrorByProjectPath,
          [requestKey]: message,
        },
        skillsLoading: hasAnyLoading({
          ...prev.skillsCatalogLoadingByProjectPath,
          [requestKey]: false,
        }),
        skillsError: message,
      }));
    }
  },

  fetchSkillDetail: async (skillId: string, projectPath?: string) => {
    if (!api.skills) return;

    const requestId = ++skillsDetailRequestSeq;
    latestSkillsDetailRequestById.set(skillId, requestId);

    set((prev) => ({
      skillsDetailLoadingById: { ...prev.skillsDetailLoadingById, [skillId]: true },
      skillsDetailErrorById: { ...prev.skillsDetailErrorById, [skillId]: null },
    }));

    try {
      const detail = await api.skills.getDetail(skillId, projectPath);
      if (latestSkillsDetailRequestById.get(skillId) !== requestId) {
        return;
      }

      set((prev) => ({
        skillsDetailsById: { ...prev.skillsDetailsById, [skillId]: detail },
        skillsDetailLoadingById: { ...prev.skillsDetailLoadingById, [skillId]: false },
        skillsDetailErrorById: { ...prev.skillsDetailErrorById, [skillId]: null },
      }));
    } catch (err) {
      if (latestSkillsDetailRequestById.get(skillId) !== requestId) {
        return;
      }

      const message = err instanceof Error ? err.message : 'Failed to load skill details';
      set((prev) => ({
        skillsDetailLoadingById: { ...prev.skillsDetailLoadingById, [skillId]: false },
        skillsDetailErrorById: { ...prev.skillsDetailErrorById, [skillId]: message },
      }));
      throw err;
    }
  },

  previewSkillUpsert: async (request: SkillUpsertRequest) => {
    if (!api.skills) {
      throw new Error('Skills API is not available');
    }

    set({ skillsMutationLoading: true, skillsMutationError: null });
    try {
      const preview = await api.skills.previewUpsert(request);
      set({ skillsMutationLoading: false });
      return preview;
    } catch (err) {
      const message = err instanceof Error ? err.message : 'Failed to review skill changes';
      set({ skillsMutationLoading: false, skillsMutationError: message });
      throw err;
    }
  },

  applySkillUpsert: async (request: SkillUpsertRequest) => {
    if (!api.skills) {
      throw new Error('Skills API is not available');
    }

    set({ skillsMutationLoading: true, skillsMutationError: null });
    try {
      const detail = await api.skills.applyUpsert(request);
      await get().fetchSkillsCatalog(request.projectPath);
      set((prev) => ({
        skillsMutationLoading: false,
        skillsDetailsById: detail?.item.id
          ? { ...prev.skillsDetailsById, [detail.item.id]: detail }
          : prev.skillsDetailsById,
      }));
      return detail;
    } catch (err) {
      const message = err instanceof Error ? err.message : 'Failed to save skill';
      set({ skillsMutationLoading: false, skillsMutationError: message });
      throw err;
    }
  },

  previewSkillImport: async (request: SkillImportRequest) => {
    if (!api.skills) {
      throw new Error('Skills API is not available');
    }

    set({ skillsMutationLoading: true, skillsMutationError: null });
    try {
      const preview = await api.skills.previewImport(request);
      set({ skillsMutationLoading: false });
      return preview;
    } catch (err) {
      const message = err instanceof Error ? err.message : 'Failed to review import changes';
      set({ skillsMutationLoading: false, skillsMutationError: message });
      throw err;
    }
  },

  applySkillImport: async (request: SkillImportRequest) => {
    if (!api.skills) {
      throw new Error('Skills API is not available');
    }

    set({ skillsMutationLoading: true, skillsMutationError: null });
    try {
      const detail = await api.skills.applyImport(request);
      await get().fetchSkillsCatalog(request.projectPath);
      set((prev) => ({
        skillsMutationLoading: false,
        skillsDetailsById: detail?.item.id
          ? { ...prev.skillsDetailsById, [detail.item.id]: detail }
          : prev.skillsDetailsById,
      }));
      return detail;
    } catch (err) {
      const message = err instanceof Error ? err.message : 'Failed to import skill';
      set({ skillsMutationLoading: false, skillsMutationError: message });
      throw err;
    }
  },

  deleteSkill: async (request: SkillDeleteRequest) => {
    if (!api.skills) {
      throw new Error('Skills API is not available');
    }

    set({ skillsMutationLoading: true, skillsMutationError: null });
    try {
      await api.skills.deleteSkill(request);
      await get().fetchSkillsCatalog(request.projectPath);
      set((prev) => {
        const nextDetails = { ...prev.skillsDetailsById };
        delete nextDetails[request.skillId];
        return {
          skillsMutationLoading: false,
          skillsDetailsById: nextDetails,
        };
      });
    } catch (err) {
      const message = err instanceof Error ? err.message : 'Failed to delete skill';
      set({ skillsMutationLoading: false, skillsMutationError: message });
      throw err;
    }
  },

  // ── Plugin install ──
  installPlugin: async (request: PluginInstallRequest) => {
    if (!api.plugins) return;

    const catalogProjectPathAtOperationStart = get().pluginCatalogProjectPath ?? undefined;
    const effectiveProjectPath =
      request.scope !== 'user'
        ? (request.projectPath ?? get().pluginCatalogProjectPath ?? undefined)
        : request.projectPath;
    const operationKey = getPluginOperationKey(
      request.pluginId,
      request.scope,
      effectiveProjectPath
    );
    const effectiveRequest =
      effectiveProjectPath === request.projectPath
        ? request
        : { ...request, projectPath: effectiveProjectPath };

    const preflightState = get();
    if (preflightState.cliStatus === null) {
      try {
        await refreshConfiguredCliStatus(preflightState);
      } catch {
        // fetchCliStatus stores the error in cliStatusError; map to a user-facing install error below.
      }
    }

    const extensionCliStatusState = getExtensionActionCliStatusState(get());
    const preflightError =
      effectiveRequest.scope !== 'user' && !effectiveRequest.projectPath
        ? PROJECT_SCOPE_REQUIRED_MESSAGE
        : getExtensionActionDisableReason({
            isInstalled: false,
            ...extensionCliStatusState,
            section: 'plugins',
          });

    if (preflightError) {
      clearPluginSuccessResetTimer(operationKey);
      set((prev) => ({
        pluginInstallProgress: { ...prev.pluginInstallProgress, [operationKey]: 'error' },
        installErrors: { ...prev.installErrors, [operationKey]: preflightError },
      }));
      return;
    }

    clearPluginSuccessResetTimer(operationKey);
    set((prev) => ({
      pluginInstallProgress: { ...prev.pluginInstallProgress, [operationKey]: 'pending' },
      installErrors: { ...prev.installErrors, [operationKey]: '' },
    }));

    try {
      const result = await api.plugins.install(effectiveRequest);
      if (result.state === 'error') {
        set((prev) => ({
          pluginInstallProgress: { ...prev.pluginInstallProgress, [operationKey]: 'error' },
          installErrors: {
            ...prev.installErrors,
            [operationKey]: result.error ?? 'Install failed',
          },
        }));
        return;
      }

      set((prev) => ({
        pluginInstallProgress: { ...prev.pluginInstallProgress, [operationKey]: 'success' },
      }));

      // Refresh catalog to pick up new installed state
      void get().fetchPluginCatalog(
        effectiveRequest.scope !== 'user'
          ? effectiveRequest.projectPath
          : catalogProjectPathAtOperationStart,
        true
      );

      schedulePluginSuccessReset(operationKey, set);
    } catch (err) {
      clearPluginSuccessResetTimer(operationKey);
      const message = err instanceof Error ? err.message : 'Install failed';
      set((prev) => ({
        pluginInstallProgress: { ...prev.pluginInstallProgress, [operationKey]: 'error' },
        installErrors: { ...prev.installErrors, [operationKey]: message },
      }));
    }
  },

  // ── Plugin uninstall ──
  uninstallPlugin: async (pluginId: string, scope?: InstallScope, projectPath?: string) => {
    if (!api.plugins) return;

    const catalogProjectPathAtOperationStart = get().pluginCatalogProjectPath ?? undefined;
    const effectiveScope = scope ?? 'user';
    const effectiveProjectPath =
      effectiveScope !== 'user'
        ? (projectPath ?? get().pluginCatalogProjectPath ?? undefined)
        : projectPath;
    const operationKey = getPluginOperationKey(pluginId, effectiveScope, effectiveProjectPath);
    if (effectiveScope !== 'user' && !effectiveProjectPath) {
      clearPluginSuccessResetTimer(operationKey);
      set((prev) => ({
        pluginInstallProgress: { ...prev.pluginInstallProgress, [operationKey]: 'error' },
        installErrors: { ...prev.installErrors, [operationKey]: PROJECT_SCOPE_REQUIRED_MESSAGE },
      }));
      return;
    }

    const preflightState = get();
    if (preflightState.cliStatus === null) {
      try {
        await refreshConfiguredCliStatus(preflightState);
      } catch {
        // fetchCliStatus stores the error in cliStatusError; map to a user-facing uninstall error below.
      }
    }

    const uninstallDisableReason = getExtensionActionDisableReason({
      isInstalled: true,
      ...getExtensionActionCliStatusState(get()),
      section: 'plugins',
    });
    if (uninstallDisableReason) {
      clearPluginSuccessResetTimer(operationKey);
      set((prev) => ({
        pluginInstallProgress: { ...prev.pluginInstallProgress, [operationKey]: 'error' },
        installErrors: { ...prev.installErrors, [operationKey]: uninstallDisableReason },
      }));
      return;
    }

    clearPluginSuccessResetTimer(operationKey);
    set((prev) => ({
      pluginInstallProgress: { ...prev.pluginInstallProgress, [operationKey]: 'pending' },
    }));

    try {
      const result = await api.plugins.uninstall(pluginId, scope, effectiveProjectPath);
      if (result.state === 'error') {
        set((prev) => ({
          pluginInstallProgress: { ...prev.pluginInstallProgress, [operationKey]: 'error' },
          installErrors: {
            ...prev.installErrors,
            [operationKey]: result.error ?? 'Uninstall failed',
          },
        }));
        return;
      }

      set((prev) => ({
        pluginInstallProgress: { ...prev.pluginInstallProgress, [operationKey]: 'success' },
      }));

      // Refresh catalog
      void get().fetchPluginCatalog(
        effectiveScope !== 'user' ? effectiveProjectPath : catalogProjectPathAtOperationStart,
        true
      );

      schedulePluginSuccessReset(operationKey, set);
    } catch (err) {
      clearPluginSuccessResetTimer(operationKey);
      const message = err instanceof Error ? err.message : 'Uninstall failed';
      set((prev) => ({
        pluginInstallProgress: { ...prev.pluginInstallProgress, [operationKey]: 'error' },
        installErrors: { ...prev.installErrors, [operationKey]: message },
      }));
    }
  },

  // ── MCP install ──
  installMcpServer: async (request: McpInstallRequest) => {
    const operationKey = getMcpOperationKey(request.registryId, request.scope, request.projectPath);
    if (!api.mcpRegistry) {
      clearMcpSuccessResetTimer(operationKey);
      set((prev) => ({
        mcpInstallProgress: { ...prev.mcpInstallProgress, [operationKey]: 'error' },
        installErrors: {
          ...prev.installErrors,
          [operationKey]: 'MCP Registry not available',
        },
      }));
      return;
    }

    const preflightState = get();
    if (preflightState.cliStatus === null) {
      try {
        await refreshConfiguredCliStatus(preflightState);
      } catch {
        // fetchCliStatus stores the error in cliStatusError; map to a user-facing install error below.
      }
    }

    const installDisableReason = getExtensionActionDisableReason({
      isInstalled: false,
      ...getExtensionActionCliStatusState(get()),
      section: 'mcp',
    });
    if (installDisableReason) {
      clearMcpSuccessResetTimer(operationKey);
      set((prev) => ({
        mcpInstallProgress: { ...prev.mcpInstallProgress, [operationKey]: 'error' },
        installErrors: { ...prev.installErrors, [operationKey]: installDisableReason },
      }));
      return;
    }

    clearMcpSuccessResetTimer(operationKey);
    set((prev) => ({
      mcpInstallProgress: { ...prev.mcpInstallProgress, [operationKey]: 'pending' },
    }));

    try {
      const result = await api.mcpRegistry.install(request);
      if (result.state === 'error') {
        set((prev) => ({
          mcpInstallProgress: { ...prev.mcpInstallProgress, [operationKey]: 'error' },
          installErrors: {
            ...prev.installErrors,
            [operationKey]: result.error ?? 'Install failed',
          },
        }));
        return;
      }

      await Promise.all([
        get().mcpFetchInstalled(request.projectPath),
        get().runMcpDiagnostics(request.projectPath),
      ]);

      set((prev) => ({
        mcpInstallProgress: { ...prev.mcpInstallProgress, [operationKey]: 'success' },
      }));

      scheduleMcpSuccessReset(operationKey, set);
    } catch (err) {
      clearMcpSuccessResetTimer(operationKey);
      const message = err instanceof Error ? err.message : 'Install failed';
      set((prev) => ({
        mcpInstallProgress: { ...prev.mcpInstallProgress, [operationKey]: 'error' },
        installErrors: { ...prev.installErrors, [operationKey]: message },
      }));
    }
  },

  // ── MCP custom install ──
  installCustomMcpServer: async (request: McpCustomInstallRequest) => {
    const operationScope = request.scope;
    const progressKey = getCustomMcpOperationKey(
      request.serverName,
      operationScope,
      request.projectPath
    );
    try {
      if (!api.mcpRegistry) {
        throw new Error('MCP Registry not available');
      }

      const preflightState = get();
      if (preflightState.cliStatus === null) {
        try {
          await refreshConfiguredCliStatus(preflightState);
        } catch {
          // fetchCliStatus stores the error in cliStatusError; map to a user-facing install error below.
        }
      }

      const installDisableReason = getExtensionActionDisableReason({
        isInstalled: false,
        ...getExtensionActionCliStatusState(get()),
        section: 'mcp',
      });
      if (installDisableReason) {
        throw new Error(installDisableReason);
      }

      clearMcpSuccessResetTimer(progressKey);
      set((prev) => ({
        mcpInstallProgress: { ...prev.mcpInstallProgress, [progressKey]: 'pending' },
      }));

      const result = await api.mcpRegistry.installCustom(request);
      if (result.state === 'error') {
        throw new Error(result.error ?? 'Install failed');
      }

      await Promise.all([
        get().mcpFetchInstalled(request.projectPath),
        get().runMcpDiagnostics(request.projectPath),
      ]);

      set((prev) => ({
        mcpInstallProgress: { ...prev.mcpInstallProgress, [progressKey]: 'success' },
      }));

      scheduleMcpSuccessReset(progressKey, set);
    } catch (err) {
      clearMcpSuccessResetTimer(progressKey);
      const message = err instanceof Error ? err.message : 'Install failed';
      set((prev) => ({
        mcpInstallProgress: { ...prev.mcpInstallProgress, [progressKey]: 'error' },
        installErrors: { ...prev.installErrors, [progressKey]: message },
      }));
      throw err instanceof Error ? err : new Error(message);
    }
  },

  // ── MCP uninstall ──
  uninstallMcpServer: async (
    registryId: string,
    name: string,
    scope?: string,
    projectPath?: string
  ) => {
    const operationScope: InstallScope =
      scope === 'global' || scope === 'user' || isProjectScopedMcpScope(scope) ? scope : 'user';
    const operationKey = getMcpOperationKey(registryId, operationScope, projectPath);
    if (!api.mcpRegistry) {
      clearMcpSuccessResetTimer(operationKey);
      set((prev) => ({
        mcpInstallProgress: { ...prev.mcpInstallProgress, [operationKey]: 'error' },
        installErrors: { ...prev.installErrors, [operationKey]: 'MCP Registry not available' },
      }));
      return;
    }

    const preflightState = get();
    if (preflightState.cliStatus === null) {
      try {
        await refreshConfiguredCliStatus(preflightState);
      } catch {
        // fetchCliStatus stores the error in cliStatusError; map to a user-facing uninstall error below.
      }
    }

    const uninstallDisableReason = getExtensionActionDisableReason({
      isInstalled: true,
      ...getExtensionActionCliStatusState(get()),
      section: 'mcp',
    });
    if (uninstallDisableReason) {
      clearMcpSuccessResetTimer(operationKey);
      set((prev) => ({
        mcpInstallProgress: { ...prev.mcpInstallProgress, [operationKey]: 'error' },
        installErrors: { ...prev.installErrors, [operationKey]: uninstallDisableReason },
      }));
      return;
    }

    clearMcpSuccessResetTimer(operationKey);
    set((prev) => ({
      mcpInstallProgress: { ...prev.mcpInstallProgress, [operationKey]: 'pending' },
    }));

    try {
      const result = await api.mcpRegistry.uninstall(name, scope, projectPath);
      if (result.state === 'error') {
        set((prev) => ({
          mcpInstallProgress: { ...prev.mcpInstallProgress, [operationKey]: 'error' },
          installErrors: {
            ...prev.installErrors,
            [operationKey]: result.error ?? 'Uninstall failed',
          },
        }));
        return;
      }

      await Promise.all([
        get().mcpFetchInstalled(projectPath),
        get().runMcpDiagnostics(projectPath),
      ]);

      set((prev) => ({
        mcpInstallProgress: { ...prev.mcpInstallProgress, [operationKey]: 'success' },
      }));

      scheduleMcpSuccessReset(operationKey, set);
    } catch (err) {
      clearMcpSuccessResetTimer(operationKey);
      const message = err instanceof Error ? err.message : 'Uninstall failed';
      set((prev) => ({
        mcpInstallProgress: { ...prev.mcpInstallProgress, [operationKey]: 'error' },
        installErrors: { ...prev.installErrors, [operationKey]: message },
      }));
    }
  },

  // ── API Keys fetch ──
  fetchApiKeys: async () => {
    if (!api.apiKeys) return;

    set({ apiKeysLoading: true, apiKeysError: null });
    try {
      const keys = await api.apiKeys.list();
      set({ apiKeys: keys, apiKeysLoading: false });
    } catch (err) {
      set({
        apiKeysLoading: false,
        apiKeysError: err instanceof Error ? err.message : 'Failed to load API keys',
      });
    }
  },

  fetchApiKeyStorageStatus: async () => {
    if (!api.apiKeys) return;
    try {
      const status = await api.apiKeys.getStorageStatus();
      set({ apiKeyStorageStatus: status });
    } catch {
      // Non-critical — UI will just not show the info icon
    }
  },

  // ── API Key save ──
  saveApiKey: async (request: ApiKeySaveRequest) => {
    if (!api.apiKeys) return;

    set({ apiKeySaving: true, apiKeysError: null });
    try {
      const savedKey = await api.apiKeys.save(request);
      const warnings: string[] = [];

      try {
        const keys = await api.apiKeys.list();
        set({ apiKeys: keys });
      } catch (listError) {
        warnings.push(
          listError instanceof Error
            ? `API key saved, but failed to refresh key list. ${listError.message}`
            : 'API key saved, but failed to refresh key list.'
        );
        set((prev) => ({
          apiKeys: upsertApiKeyEntry(prev.apiKeys, savedKey),
        }));
      }

      await refreshConfiguredCliStatus(get());
      const refreshError = get().cliStatusError;
      if (refreshError) {
        warnings.push(`API key saved, but failed to refresh provider status. ${refreshError}`);
      }
      set({ apiKeySaving: false, apiKeysError: warnings.length > 0 ? warnings.join(' ') : null });
    } catch (err) {
      set({
        apiKeySaving: false,
        apiKeysError: err instanceof Error ? err.message : 'Failed to save API key',
      });
      throw err; // Re-throw so the dialog can show the error
    }
  },

  // ── API Key delete ──
  deleteApiKey: async (id: string) => {
    if (!api.apiKeys) return;

    try {
      await api.apiKeys.delete(id);
      set((prev) => ({
        apiKeys: prev.apiKeys.filter((k) => k.id !== id),
      }));
      await refreshConfiguredCliStatus(get());
      const refreshError = get().cliStatusError;
      set({
        apiKeysError: refreshError
          ? `API key deleted, but failed to refresh provider status. ${refreshError}`
          : null,
      });
    } catch (err) {
      set({
        apiKeysError: err instanceof Error ? err.message : 'Failed to delete API key',
      });
      throw err;
    }
  },

  // ── Tab opener ──
  openExtensionsTab: () => {
    const state = get();
    const currentProjectId = state.selectedProjectId ?? state.activeProjectId ?? undefined;
    const focusedPane = state.paneLayout.panes.find((p) => p.id === state.paneLayout.focusedPaneId);
    const existingTab = focusedPane?.tabs.find((tab) => tab.type === 'extensions');
    if (existingTab) {
      // Update projectId to reflect the currently selected project
      if (existingTab.projectId !== currentProjectId || existingTab.label !== '扩展') {
        const pane = findPaneByTabId(state.paneLayout, existingTab.id);
        if (pane) {
          set({
            paneLayout: updatePane(state.paneLayout, {
              ...pane,
              tabs: pane.tabs.map((t) =>
                t.id === existingTab.id ? { ...t, label: '扩展', projectId: currentProjectId } : t
              ),
            }),
          });
        }
      }
      state.setActiveTab(existingTab.id);
      return;
    }

    state.openTab({
      type: 'extensions',
      label: '扩展',
      projectId: currentProjectId,
    });
  },

  // ── GitHub Stars (fire-and-forget) ──
  fetchMcpGitHubStars: (repositoryUrls: string[]) => {
    if (!api.mcpRegistry || repositoryUrls.length === 0) return;
    void api.mcpRegistry
      .githubStars(repositoryUrls)
      .then((stars) => {
        if (Object.keys(stars).length > 0) {
          set((prev) => ({
            mcpGitHubStars: { ...prev.mcpGitHubStars, ...stars },
          }));
        }
      })
      .catch(() => {
        // Silent failure — stars are supplementary data
      });
  },
});
