// PluginAPI factory â€” builds the sandboxed API facade for each plugin

import type {
  Disposable,
  InstalledPlugin,
  Permission,
  ToolbarAction,
  BannerFactory,
  SettingsSection,
  ComposerAction,
  SidebarWidget,
  ContextMenuItem,
  KeyboardShortcut,
  AdminPageSection,
  CalendarEventAction,
  SlotName,
  PluginI18n,
} from './plugin-types';
import { IMPLICIT_PERMISSIONS as IMPLICIT } from './plugin-types';
import {
  emailHooks, calendarHooks, calendarFormHooks, contactHooks, fileHooks,
  authHooks, settingsHooks, identityHooks, filterHooks,
  taskHooks, templateHooks, smimeHooks, vacationHooks,
  uiHooks, themeHooks, toastHooks, dragDropHooks,
  keyboardHooks, appLifecycleHooks, accountSecurityHooks,
  sidebarAppHooks, avatarHooks, renderHooks, routerHooks,
} from './plugin-hooks';
import { createPluginI18n } from './plugin-i18n';
import { toast as appToast } from '@/stores/toast-store';
import { useAuthStore } from '@/stores/auth-store';
import { apiFetch } from '@/lib/browser-navigation';

// --- Permission helpers --------------------------------------

// eslint-disable-next-line @typescript-eslint/no-explicit-any
function getPluginExternals(): any {
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  return (globalThis as any).__PLUGIN_EXTERNALS__;
}

function hasPermission(plugin: InstalledPlugin, perm: Permission): boolean {
  if ((IMPLICIT as readonly string[]).includes(perm)) return true;
  return plugin.permissions.includes(perm);
}

function requirePermission(plugin: InstalledPlugin, perm: Permission): void {
  if (!hasPermission(plugin, perm)) {
    throw new Error(`Plugin "${plugin.id}" lacks permission "${perm}"`);
  }
}

/** Returns a no-op disposable when permission is missing (silent failure) */
function guardedHook<T extends (...args: never[]) => unknown>(
  plugin: InstalledPlugin,
  perm: Permission,
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  bus: { register: (pluginId: string, handler: any, order?: number) => Disposable },
  handler: T,
  order: number = 100,
): Disposable {
  if (!hasPermission(plugin, perm)) {
    return { dispose: () => {} };
  }
  return bus.register(plugin.id, handler, order);
}

// --- Plugin-scoped storage -----------------------------------

function createPluginStorage(pluginId: string) {
  const prefix = `plugin:${pluginId}:`;

  return {
    get: <T>(key: string): T | null => {
      if (typeof window === 'undefined') return null;
      const raw = localStorage.getItem(prefix + key);
      if (raw === null) return null;
      try { return JSON.parse(raw) as T; } catch { return null; }
    },
    set: <T>(key: string, value: T): void => {
      if (typeof window === 'undefined') return;
      localStorage.setItem(prefix + key, JSON.stringify(value));
    },
    remove: (key: string): void => {
      if (typeof window === 'undefined') return;
      localStorage.removeItem(prefix + key);
    },
    keys: (): string[] => {
      if (typeof window === 'undefined') return [];
      const keys: string[] = [];
      for (let i = 0; i < localStorage.length; i++) {
        const k = localStorage.key(i);
        if (k?.startsWith(prefix)) keys.push(k.slice(prefix.length));
      }
      return keys;
    },
  };
}

// --- Plugin-scoped logger ------------------------------------

function createPluginLogger(pluginId: string) {
  const tag = `[plugin:${pluginId}]`;
  return {
    debug: (...args: unknown[]) => console.debug(tag, ...args),
    info: (...args: unknown[]) => console.info(tag, ...args),
    warn: (...args: unknown[]) => console.warn(tag, ...args),
    error: (...args: unknown[]) => console.error(tag, ...args),
  };
}

// --- Cross-origin fetch helpers ------------------------------

/**
 * Returns true when `url`'s origin is allowed by one of the plugin's
 * declared `httpOrigins` patterns. Patterns are either a literal origin
 * (`https://host[:port]`) or a wildcard subdomain form (`https://*.host`).
 *
 * Wildcards match exactly one subdomain layer above `host` - e.g.
 * `https://*.example.com` matches `https://a.example.com` but NOT
 * `https://example.com` and NOT `https://a.b.example.com`. This mirrors how
 * the CSP frame-src handles wildcards and avoids accidentally widening
 * access when the manifest only intended a single tier.
 */
function originMatchesAllowlist(url: URL, allowlist: string[]): boolean {
  if (url.protocol !== 'https:') return false;
  for (const entry of allowlist) {
    let parsed: URL;
    try {
      parsed = new URL(entry.replace('*.', ''));
    } catch {
      continue;
    }
    if (parsed.protocol !== 'https:') continue;
    const port = url.port || '';
    const expectedPort = parsed.port || '';
    if (port !== expectedPort) continue;
    if (entry.includes('*.')) {
      const suffix = '.' + parsed.hostname.toLowerCase();
      if (url.hostname.toLowerCase().endsWith(suffix)) {
        const prefix = url.hostname.slice(0, url.hostname.length - suffix.length);
        // Require exactly one non-empty subdomain label.
        if (prefix.length > 0 && !prefix.includes('.')) return true;
      }
    } else {
      if (url.hostname.toLowerCase() === parsed.hostname.toLowerCase()) return true;
    }
  }
  return false;
}

// --- Cross-origin fetch types --------------------------------

export interface PluginFetchInit {
  /** HTTP method. Defaults to GET. */
  method?: string;
  /** Request headers. Plain object only - no Headers / cookies forwarded. */
  headers?: Record<string, string>;
  /** Body. Plain string, ArrayBuffer, Uint8Array, Blob, or FormData. */
  body?: string | ArrayBuffer | ArrayBufferView | Blob | FormData | null;
  /** Optional AbortSignal for cancellation. */
  signal?: AbortSignal;
}

export interface PluginFetchResponse {
  ok: boolean;
  status: number;
  statusText: string;
  /** Response headers, lower-cased keys. */
  headers: Record<string, string>;
  /** Resolves the body as text. */
  text: () => Promise<string>;
  /** Resolves the body as parsed JSON, or null on parse error. */
  json: () => Promise<unknown>;
  /** Resolves the body as raw bytes. */
  arrayBuffer: () => Promise<ArrayBuffer>;
  /** Resolves the body as a Blob. */
  blob: () => Promise<Blob>;
}

// --- PluginAPI interface -------------------------------------

export interface PluginAPI {
  plugin: { id: string; version: string; settings: Record<string, unknown> };
  /** Localisation API - register translations and call t() to get strings */
  i18n: PluginI18n;
  ui: {
    registerToolbarAction: (action: ToolbarAction) => Disposable;
    registerEmailBanner: (factory: BannerFactory) => Disposable;
    registerEmailFooter: (component: React.ComponentType) => Disposable;
    registerSettingsSection: (section: SettingsSection) => Disposable;
    registerComposerAction: (action: ComposerAction) => Disposable;
    registerSidebarWidget: (widget: SidebarWidget) => Disposable;
    registerComposerSidebar: (widget: SidebarWidget) => Disposable;
    registerDetailSidebar: (widget: SidebarWidget) => Disposable;
    registerContextMenuItem: (item: ContextMenuItem) => Disposable;
    registerNavigationRailItem: (component: React.ComponentType) => Disposable;
    registerCalendarEventAction: (action: CalendarEventAction) => Disposable;
    registerAdminPage: (page: AdminPageSection) => Disposable;
  };
  hooks: PluginHooksAPI;
  toast: {
    success: (message: string) => void;
    error: (message: string) => void;
    info: (message: string) => void;
    warning: (message: string) => void;
  };
  http: {
    post: (path: string, body: Record<string, unknown>) => Promise<{ ok: boolean; status: number; data: unknown }>;
    /**
     * Cross-origin fetch against an origin declared in the manifest's
     * `httpOrigins` allowlist. Requires `http:fetch` permission.
     *
     * No webmail credentials are forwarded - the plugin must supply its own
     * `Authorization` (or other auth) header. Each call is gated on origin
     * even when the URL came from plugin settings, so a user-pasted URL
     * outside the allowlist is rejected at the boundary.
     */
    fetch: (url: string, init?: PluginFetchInit) => Promise<PluginFetchResponse>;
  };
  storage: ReturnType<typeof createPluginStorage>;
  log: ReturnType<typeof createPluginLogger>;
  admin: {
    getConfig: (key: string) => Promise<unknown>;
    getAllConfig: () => Promise<Record<string, unknown>>;
    setConfig: (key: string, value: unknown) => Promise<void>;
    deleteConfig: (key: string) => Promise<void>;
  };
}

// Simplified hooks API type (all hooks return Disposable)
export interface PluginHooksAPI {
  // Email
  onEmailOpen: (handler: (...args: unknown[]) => unknown) => Disposable;
  onEmailClose: (handler: () => void) => Disposable;
  onEmailContentRender: (handler: (...args: unknown[]) => unknown) => Disposable;
  onThreadExpand: (handler: (...args: unknown[]) => unknown) => Disposable;
  /** Intercept - receives ComposeOptions, may mutate fields, return false to cancel */
  onBeforeCompose: (handler: (options: import('./plugin-types').ComposeOptions) => boolean | void | Promise<boolean | void>) => Disposable;
  onComposerOpen: (handler: (...args: unknown[]) => unknown) => Disposable;
  onBeforeEmailSend: (handler: (...args: unknown[]) => unknown) => Disposable;
  onAfterEmailSend: (handler: (...args: unknown[]) => unknown) => Disposable;
  onDraftAutoSave: (handler: (...args: unknown[]) => unknown) => Disposable;
  onBeforeEmailDelete: (handler: (...args: unknown[]) => unknown) => Disposable;
  onAfterEmailDelete: (handler: (...args: unknown[]) => unknown) => Disposable;
  onBeforeEmailMove: (handler: (...args: unknown[]) => unknown) => Disposable;
  onAfterEmailMove: (handler: (...args: unknown[]) => unknown) => Disposable;
  /** Emitted after emails are moved to the Archive mailbox */
  onEmailArchive: (handler: (emailIds: string[]) => void) => Disposable;
  /** Emitted after emails are moved out of the Archive mailbox */
  onEmailUnarchive: (handler: (emailIds: string[]) => void) => Disposable;
  onEmailReadStateChange: (handler: (...args: unknown[]) => unknown) => Disposable;
  onEmailStarToggle: (handler: (...args: unknown[]) => unknown) => Disposable;
  onEmailSpamToggle: (handler: (...args: unknown[]) => unknown) => Disposable;
  onEmailKeywordChange: (handler: (...args: unknown[]) => unknown) => Disposable;
  onMailboxChange: (handler: (...args: unknown[]) => unknown) => Disposable;
  onMailboxesRefresh: (handler: (...args: unknown[]) => unknown) => Disposable;
  onMailboxCreate: (handler: (...args: unknown[]) => unknown) => Disposable;
  onMailboxRename: (handler: (...args: unknown[]) => unknown) => Disposable;
  onMailboxDelete: (handler: (...args: unknown[]) => unknown) => Disposable;
  onMailboxEmpty: (handler: (...args: unknown[]) => unknown) => Disposable;
  onSearch: (handler: (...args: unknown[]) => unknown) => Disposable;
  onSearchResults: (handler: (...args: unknown[]) => unknown) => Disposable;
  onEmailSelectionChange: (handler: (...args: unknown[]) => unknown) => Disposable;
  onNewEmailReceived: (handler: (...args: unknown[]) => unknown) => Disposable;
  onPushConnectionChange: (handler: (...args: unknown[]) => unknown) => Disposable;
  onQuotaChange: (handler: (...args: unknown[]) => unknown) => Disposable;
  /** Intercept - receives MailtoContext, return false to prevent the system mail client */
  onMailtoIntercept: (handler: (ctx: import('./plugin-types').MailtoContext) => boolean | void | Promise<boolean | void>) => Disposable;
  /** Transform - receives the OutgoingEmail and returns a (possibly modified) copy */
  onTransformOutgoingEmail: (handler: (email: import('./plugin-types').OutgoingEmail) => import('./plugin-types').OutgoingEmail | void | Promise<import('./plugin-types').OutgoingEmail | void>) => Disposable;
  /** Intercept - receives ReplyContext, return false to cancel */
  onBeforeReply: (handler: (ctx: import('./plugin-types').ReplyContext) => boolean | void | Promise<boolean | void>) => Disposable;
  onBeforeReplyAll: (handler: (ctx: import('./plugin-types').ReplyContext) => boolean | void | Promise<boolean | void>) => Disposable;
  onBeforeForward: (handler: (ctx: import('./plugin-types').ReplyContext) => boolean | void | Promise<boolean | void>) => Disposable;
  /** Intercept - receives AttachmentInfo, return false to refuse the upload */
  onBeforeAttachmentUpload: (handler: (info: import('./plugin-types').AttachmentInfo) => boolean | void | Promise<boolean | void>) => Disposable;
  onAfterAttachmentUpload: (handler: (info: import('./plugin-types').AttachmentInfo) => void) => Disposable;
  onAttachmentDownload: (handler: (info: import('./plugin-types').AttachmentInfo) => void) => Disposable;
  /** Transform - receives AttachmentPreview, may return a modified preview */
  onAttachmentPreview: (handler: (preview: import('./plugin-types').AttachmentPreview, info: import('./plugin-types').AttachmentInfo) => import('./plugin-types').AttachmentPreview | void | Promise<import('./plugin-types').AttachmentPreview | void>) => Disposable;
  /** Transform - receives ExternalSearchResult[] and returns an extended array */
  onProvideSearchResults: (handler: (results: import('./plugin-types').ExternalSearchResult[], ctx: { query: string; filters: import('./plugin-types').SearchFilters }) => import('./plugin-types').ExternalSearchResult[] | void | Promise<import('./plugin-types').ExternalSearchResult[] | void>) => Disposable;
  /** Observer - debounced snapshot of the composer draft */
  onDraftChange: (handler: (draft: import('./plugin-types').DraftView) => void) => Disposable;
  // Calendar
  onCalendarEventOpen: (handler: (...args: unknown[]) => unknown) => Disposable;
  onBeforeEventCreate: (handler: (...args: unknown[]) => unknown) => Disposable;
  onAfterEventCreate: (handler: (...args: unknown[]) => unknown) => Disposable;
  onBeforeEventUpdate: (handler: (...args: unknown[]) => unknown) => Disposable;
  onAfterEventUpdate: (handler: (...args: unknown[]) => unknown) => Disposable;
  onBeforeEventDelete: (handler: (...args: unknown[]) => unknown) => Disposable;
  onAfterEventDelete: (handler: (...args: unknown[]) => unknown) => Disposable;
  onEventRsvp: (handler: (...args: unknown[]) => unknown) => Disposable;
  onEventsImport: (handler: (...args: unknown[]) => unknown) => Disposable;
  onCalendarDateChange: (handler: (...args: unknown[]) => unknown) => Disposable;
  onCalendarViewChange: (handler: (...args: unknown[]) => unknown) => Disposable;
  onCalendarChange: (handler: (...args: unknown[]) => unknown) => Disposable;
  onCalendarVisibilityToggle: (handler: (...args: unknown[]) => unknown) => Disposable;
  onICalSubscriptionChange: (handler: (...args: unknown[]) => unknown) => Disposable;
  onCalendarAlert: (handler: (...args: unknown[]) => unknown) => Disposable;
  onCalendarAlertAcknowledge: (handler: (...args: unknown[]) => unknown) => Disposable;
  /** Transform - receives ConflictWarning[] and returns an extended array */
  onCheckEventConflicts: (handler: (warnings: import('./plugin-types').ConflictWarning[], ctx: { event: import('./plugin-types').CalendarEventFormView }) => import('./plugin-types').ConflictWarning[] | void | Promise<import('./plugin-types').ConflictWarning[] | void>) => Disposable;
  // Calendar Form
  onCalendarEventFormOpen: (handler: (...args: unknown[]) => unknown) => Disposable;
  onCalendarEventFormSave: (handler: (...args: unknown[]) => unknown) => Disposable;
  // Contacts
  onContactOpen: (handler: (...args: unknown[]) => unknown) => Disposable;
  onBeforeContactCreate: (handler: (...args: unknown[]) => unknown) => Disposable;
  onAfterContactCreate: (handler: (...args: unknown[]) => unknown) => Disposable;
  onBeforeContactUpdate: (handler: (...args: unknown[]) => unknown) => Disposable;
  onAfterContactUpdate: (handler: (...args: unknown[]) => unknown) => Disposable;
  onBeforeContactDelete: (handler: (...args: unknown[]) => unknown) => Disposable;
  onAfterContactDelete: (handler: (...args: unknown[]) => unknown) => Disposable;
  onContactsImport: (handler: (...args: unknown[]) => unknown) => Disposable;
  onContactSelectionChange: (handler: (...args: unknown[]) => unknown) => Disposable;
  onContactGroupChange: (handler: (...args: unknown[]) => unknown) => Disposable;
  onContactGroupMemberChange: (handler: (...args: unknown[]) => unknown) => Disposable;
  onContactMove: (handler: (...args: unknown[]) => unknown) => Disposable;
  /** Transform - receives RecipientSuggestion[] and returns an extended array */
  onProvideRecipientSuggestions: (handler: (suggestions: import('./plugin-types').RecipientSuggestion[], ctx: { query: string }) => import('./plugin-types').RecipientSuggestion[] | void | Promise<import('./plugin-types').RecipientSuggestion[] | void>) => Disposable;
  // Files
  onFileNavigate: (handler: (...args: unknown[]) => unknown) => Disposable;
  onBeforeFileUpload: (handler: (...args: unknown[]) => unknown) => Disposable;
  onAfterFileUpload: (handler: (...args: unknown[]) => unknown) => Disposable;
  onFileDownload: (handler: (...args: unknown[]) => unknown) => Disposable;
  onFileUploadCancel: (handler: (...args: unknown[]) => unknown) => Disposable;
  onDirectoryCreate: (handler: (...args: unknown[]) => unknown) => Disposable;
  onBeforeFileDelete: (handler: (...args: unknown[]) => unknown) => Disposable;
  onAfterFileDelete: (handler: (...args: unknown[]) => unknown) => Disposable;
  /** Intercept - receives { file: FileResourceView, newName: string }, return false to cancel */
  onBeforeFileRename: (handler: (ctx: { file: import('./plugin-types').FileResourceView; newName: string }) => boolean | void | Promise<boolean | void>) => Disposable;
  onFileRename: (handler: (...args: unknown[]) => unknown) => Disposable;
  onFileMove: (handler: (...args: unknown[]) => unknown) => Disposable;
  onFileCopy: (handler: (...args: unknown[]) => unknown) => Disposable;
  onFileDuplicate: (handler: (...args: unknown[]) => unknown) => Disposable;
  onFileFavoriteToggle: (handler: (...args: unknown[]) => unknown) => Disposable;
  onFileSelectionChange: (handler: (...args: unknown[]) => unknown) => Disposable;
  onFileUndo: (handler: (...args: unknown[]) => unknown) => Disposable;
  // Auth
  onLogin: (handler: (...args: unknown[]) => unknown) => Disposable;
  onBeforeLogout: (handler: () => void) => Disposable;
  onAfterLogout: (handler: () => void) => Disposable;
  onAccountSwitch: (handler: (...args: unknown[]) => unknown) => Disposable;
  onAccountAdd: (handler: (...args: unknown[]) => unknown) => Disposable;
  onAccountRemove: (handler: (...args: unknown[]) => unknown) => Disposable;
  onTokenRefresh: (handler: () => void) => Disposable;
  onAuthReady: (handler: (...args: unknown[]) => unknown) => Disposable;
  // Settings
  onSettingChange: (handler: (...args: unknown[]) => unknown) => Disposable;
  onSettingsExport: (handler: () => void) => Disposable;
  onSettingsImport: (handler: (...args: unknown[]) => unknown) => Disposable;
  onSettingsReset: (handler: () => void) => Disposable;
  onSettingsSync: (handler: (...args: unknown[]) => unknown) => Disposable;
  onKeywordChange: (handler: (...args: unknown[]) => unknown) => Disposable;
  onTrustedSenderChange: (handler: (...args: unknown[]) => unknown) => Disposable;
  // Identity
  onIdentitiesLoaded: (handler: (...args: unknown[]) => unknown) => Disposable;
  onIdentityCreate: (handler: (...args: unknown[]) => unknown) => Disposable;
  onIdentityUpdate: (handler: (...args: unknown[]) => unknown) => Disposable;
  onIdentityDelete: (handler: (...args: unknown[]) => unknown) => Disposable;
  onIdentitySelect: (handler: (...args: unknown[]) => unknown) => Disposable;
  onSignatureRender: (handler: (...args: unknown[]) => unknown) => Disposable;
  // Filters
  onFiltersLoaded: (handler: (...args: unknown[]) => unknown) => Disposable;
  onFilterRuleChange: (handler: (...args: unknown[]) => unknown) => Disposable;
  onFiltersSave: (handler: (...args: unknown[]) => unknown) => Disposable;
  onSieveScriptChange: (handler: (...args: unknown[]) => unknown) => Disposable;
  // Tasks
  onTasksLoaded: (handler: (...args: unknown[]) => unknown) => Disposable;
  onTaskCreate: (handler: (...args: unknown[]) => unknown) => Disposable;
  onTaskUpdate: (handler: (...args: unknown[]) => unknown) => Disposable;
  onTaskDelete: (handler: (...args: unknown[]) => unknown) => Disposable;
  onTaskToggleComplete: (handler: (...args: unknown[]) => unknown) => Disposable;
  onTaskFilterChange: (handler: (...args: unknown[]) => unknown) => Disposable;
  // Templates
  onTemplateCreate: (handler: (...args: unknown[]) => unknown) => Disposable;
  onTemplateUpdate: (handler: (...args: unknown[]) => unknown) => Disposable;
  onTemplateDelete: (handler: (...args: unknown[]) => unknown) => Disposable;
  onTemplateApply: (handler: (...args: unknown[]) => unknown) => Disposable;
  onTemplatesImport: (handler: (...args: unknown[]) => unknown) => Disposable;
  onTemplateRender: (handler: (...args: unknown[]) => unknown) => Disposable;
  // S/MIME
  onSmimeKeyImport: (handler: (...args: unknown[]) => unknown) => Disposable;
  onSmimeCertImport: (handler: (...args: unknown[]) => unknown) => Disposable;
  onSmimeKeyStateChange: (handler: (...args: unknown[]) => unknown) => Disposable;
  onSmimeDefaultsChange: (handler: (...args: unknown[]) => unknown) => Disposable;
  // Vacation
  onVacationLoaded: (handler: (...args: unknown[]) => unknown) => Disposable;
  onVacationUpdate: (handler: (...args: unknown[]) => unknown) => Disposable;
  // UI
  onViewChange: (handler: (...args: unknown[]) => unknown) => Disposable;
  onSidebarToggle: (handler: (...args: unknown[]) => unknown) => Disposable;
  onSidebarCollapse: (handler: (...args: unknown[]) => unknown) => Disposable;
  onDeviceTypeChange: (handler: (...args: unknown[]) => unknown) => Disposable;
  onColumnResize: (handler: (...args: unknown[]) => unknown) => Disposable;
  onMobileBack: (handler: () => void) => Disposable;
  onMobileViewSwitch: (handler: (...args: unknown[]) => unknown) => Disposable;
  /** Intercept - receives ExternalLinkContext, return false to cancel navigation */
  onBeforeExternalLink: (handler: (ctx: import('./plugin-types').ExternalLinkContext) => boolean | void | Promise<boolean | void>) => Disposable;
  /** Observer - debounced text-selection change */
  onTextSelectionChange: (handler: (ctx: import('./plugin-types').SelectionContext) => void) => Disposable;
  // Theme
  onThemeChange: (handler: (...args: unknown[]) => unknown) => Disposable;
  onCustomThemeChange: (handler: (...args: unknown[]) => unknown) => Disposable;
  onLocaleChange: (handler: (...args: unknown[]) => unknown) => Disposable;
  // Toast
  onToastShow: (handler: (...args: unknown[]) => unknown) => Disposable;
  onToastDismiss: (handler: (...args: unknown[]) => unknown) => Disposable;
  onBrowserNotification: (handler: (...args: unknown[]) => unknown) => Disposable;
  /** Observer fired when an OS-level notification is clicked */
  onNotificationClick: (handler: (ctx: { tag: string; data?: unknown }) => void) => Disposable;
  // Drag & Drop
  onDragStart: (handler: (...args: unknown[]) => unknown) => Disposable;
  onDragEnd: (handler: (...args: unknown[]) => unknown) => Disposable;
  onEmailDrop: (handler: (...args: unknown[]) => unknown) => Disposable;
  onTagDrop: (handler: (...args: unknown[]) => unknown) => Disposable;
  // Keyboard
  registerShortcut: (shortcut: KeyboardShortcut) => Disposable;
  onBeforeShortcut: (handler: (...args: unknown[]) => unknown) => Disposable;
  onAfterShortcut: (handler: (...args: unknown[]) => unknown) => Disposable;
  // App Lifecycle
  onAppReady: (handler: () => void) => Disposable;
  onVisibilityChange: (handler: (...args: unknown[]) => unknown) => Disposable;
  onBeforeUnload: (handler: () => void) => Disposable;
  onAppError: (handler: (...args: unknown[]) => unknown) => Disposable;
  onInterval: (handler: () => void, intervalMs: number) => Disposable;
  /** Observer - browser window focus / blur */
  onWindowFocus: (handler: () => void) => Disposable;
  onWindowBlur: (handler: () => void) => Disposable;
  /** Observer - network connectivity transitions */
  onOnline: (handler: () => void) => Disposable;
  onOffline: (handler: () => void) => Disposable;
  // Account Security
  onPasswordChange: (handler: () => void) => Disposable;
  onTotpChange: (handler: (...args: unknown[]) => unknown) => Disposable;
  onAppPasswordChange: (handler: (...args: unknown[]) => unknown) => Disposable;
  onEncryptionChange: (handler: () => void) => Disposable;
  onDisplayNameChange: (handler: (...args: unknown[]) => unknown) => Disposable;
  // Sidebar Apps
  onSidebarAppOpen: (handler: (...args: unknown[]) => unknown) => Disposable;
  onSidebarAppClose: (handler: (...args: unknown[]) => unknown) => Disposable;
  onSidebarAppChange: (handler: (...args: unknown[]) => unknown) => Disposable;
  // Avatar
  onAvatarResolve: (handler: (...args: unknown[]) => unknown) => Disposable;
  // Render - transform hook for email list row badges
  // Handler: (badges: EmailListBadge[], ctx: { emailId: string; email: EmailReadView }) => EmailListBadge[]
  onEmailListItemRender: (handler: (...args: unknown[]) => unknown) => Disposable;
  // Router
  /** Observer - fired on every in-app navigation. RouteContext.from holds the previous path. */
  onNavigate: (handler: (ctx: import('./plugin-types').RouteContext) => void) => Disposable;
  onRouteEnter: (handler: (ctx: import('./plugin-types').RouteContext) => void) => Disposable;
  onRouteLeave: (handler: (ctx: import('./plugin-types').RouteContext) => void) => Disposable;
}

// --- Permission mapping for hooks ----------------------------

const HOOK_PERMISSIONS: Record<string, Permission> = {
  // Email
  onEmailOpen: 'email:read', onEmailClose: 'email:read',
  onEmailContentRender: 'email:read', onThreadExpand: 'email:read',
  onBeforeCompose: 'email:read', onComposerOpen: 'email:read',
  onDraftAutoSave: 'email:read',
  onMailboxChange: 'email:read', onMailboxesRefresh: 'email:read',
  onSearch: 'email:read', onSearchResults: 'email:read',
  onEmailSelectionChange: 'email:read', onNewEmailReceived: 'email:read',
  onPushConnectionChange: 'email:read', onQuotaChange: 'email:read',
  onMailtoIntercept: 'email:read', onEmailListItemRender: 'email:read',
  onBeforeReply: 'email:read', onBeforeReplyAll: 'email:read',
  onBeforeForward: 'email:read', onAttachmentDownload: 'email:read',
  onAttachmentPreview: 'email:read', onProvideSearchResults: 'email:read',
  onDraftChange: 'email:read',
  onBeforeAttachmentUpload: 'email:write', onAfterAttachmentUpload: 'email:write',
  onBeforeEmailSend: 'email:send', onAfterEmailSend: 'email:send',
  onTransformOutgoingEmail: 'email:send',
  onBeforeEmailDelete: 'email:write', onAfterEmailDelete: 'email:write',
  onBeforeEmailMove: 'email:write', onAfterEmailMove: 'email:write',
  onEmailArchive: 'email:write', onEmailUnarchive: 'email:write',
  onEmailReadStateChange: 'email:write', onEmailStarToggle: 'email:write',
  onEmailSpamToggle: 'email:write', onEmailKeywordChange: 'email:write',
  onMailboxCreate: 'email:write', onMailboxRename: 'email:write',
  onMailboxDelete: 'email:write', onMailboxEmpty: 'email:write',
  // Calendar
  onCalendarEventOpen: 'calendar:read', onCalendarDateChange: 'calendar:read',
  onCalendarViewChange: 'calendar:read', onCalendarVisibilityToggle: 'calendar:read',
  onCalendarAlert: 'calendar:read', onCalendarAlertAcknowledge: 'calendar:read',
  onCheckEventConflicts: 'calendar:read',
  onCalendarEventFormOpen: 'calendar:read', onCalendarEventFormSave: 'calendar:write',
  onBeforeEventCreate: 'calendar:write', onAfterEventCreate: 'calendar:write',
  onBeforeEventUpdate: 'calendar:write', onAfterEventUpdate: 'calendar:write',
  onBeforeEventDelete: 'calendar:write', onAfterEventDelete: 'calendar:write',
  onEventRsvp: 'calendar:write', onEventsImport: 'calendar:write',
  onCalendarChange: 'calendar:write', onICalSubscriptionChange: 'calendar:write',
  // Contacts
  onContactOpen: 'contacts:read', onContactSelectionChange: 'contacts:read',
  onProvideRecipientSuggestions: 'contacts:read',
  onBeforeContactCreate: 'contacts:write', onAfterContactCreate: 'contacts:write',
  onBeforeContactUpdate: 'contacts:write', onAfterContactUpdate: 'contacts:write',
  onBeforeContactDelete: 'contacts:write', onAfterContactDelete: 'contacts:write',
  onContactsImport: 'contacts:write', onContactGroupChange: 'contacts:write',
  onContactGroupMemberChange: 'contacts:write', onContactMove: 'contacts:write',
  // Files
  onFileNavigate: 'files:read', onFileDownload: 'files:read', onFileSelectionChange: 'files:read',
  onBeforeFileUpload: 'files:write', onAfterFileUpload: 'files:write',
  onFileUploadCancel: 'files:write', onDirectoryCreate: 'files:write',
  onBeforeFileDelete: 'files:write', onAfterFileDelete: 'files:write',
  onBeforeFileRename: 'files:write',
  onFileRename: 'files:write', onFileMove: 'files:write', onFileCopy: 'files:write',
  onFileDuplicate: 'files:write', onFileFavoriteToggle: 'files:write', onFileUndo: 'files:write',
  // Auth
  onLogin: 'auth:observe', onBeforeLogout: 'auth:observe', onAfterLogout: 'auth:observe',
  onAccountSwitch: 'auth:observe', onAccountAdd: 'auth:observe', onAccountRemove: 'auth:observe',
  onTokenRefresh: 'auth:observe', onAuthReady: 'auth:observe',
  // Settings
  onSettingChange: 'settings:read', onSettingsExport: 'settings:read',
  onSettingsImport: 'settings:read', onSettingsReset: 'settings:read',
  onSettingsSync: 'settings:read', onKeywordChange: 'settings:read',
  onTrustedSenderChange: 'settings:read',
  // Identity
  onIdentitiesLoaded: 'identity:read', onIdentitySelect: 'identity:read',
  onSignatureRender: 'identity:read',
  onIdentityCreate: 'identity:write', onIdentityUpdate: 'identity:write',
  onIdentityDelete: 'identity:write',
  // Filters
  onFiltersLoaded: 'filters:read',
  onFilterRuleChange: 'filters:write', onFiltersSave: 'filters:write',
  onSieveScriptChange: 'filters:write',
  // Tasks
  onTasksLoaded: 'tasks:read', onTaskFilterChange: 'tasks:read',
  onTaskCreate: 'tasks:write', onTaskUpdate: 'tasks:write',
  onTaskDelete: 'tasks:write', onTaskToggleComplete: 'tasks:write',
  // Templates
  onTemplateApply: 'templates:read', onTemplateRender: 'templates:read',
  onTemplateCreate: 'templates:write', onTemplateUpdate: 'templates:write',
  onTemplateDelete: 'templates:write', onTemplatesImport: 'templates:write',
  // S/MIME
  onSmimeKeyImport: 'smime:read', onSmimeCertImport: 'smime:read',
  onSmimeKeyStateChange: 'smime:read', onSmimeDefaultsChange: 'smime:read',
  // Vacation
  onVacationLoaded: 'vacation:read', onVacationUpdate: 'vacation:write',
  // UI
  onViewChange: 'ui:observe', onSidebarToggle: 'ui:observe',
  onSidebarCollapse: 'ui:observe', onDeviceTypeChange: 'ui:observe',
  onColumnResize: 'ui:observe', onMobileBack: 'ui:observe',
  onMobileViewSwitch: 'ui:observe',
  onBeforeExternalLink: 'ui:observe', onTextSelectionChange: 'ui:observe',
  // Theme
  onThemeChange: 'ui:observe', onCustomThemeChange: 'ui:observe',
  onLocaleChange: 'ui:observe',
  // Toast
  onToastShow: 'ui:observe', onToastDismiss: 'ui:observe',
  onBrowserNotification: 'ui:observe', onNotificationClick: 'ui:observe',
  // Drag & Drop
  onDragStart: 'ui:observe', onDragEnd: 'ui:observe',
  onEmailDrop: 'ui:observe', onTagDrop: 'ui:observe',
  // Keyboard
  registerShortcut: 'ui:keyboard', onBeforeShortcut: 'ui:keyboard',
  onAfterShortcut: 'ui:keyboard',
  // App Lifecycle
  onAppReady: 'app:lifecycle', onVisibilityChange: 'app:lifecycle',
  onBeforeUnload: 'app:lifecycle', onAppError: 'app:lifecycle',
  onInterval: 'app:lifecycle',
  onWindowFocus: 'app:lifecycle', onWindowBlur: 'app:lifecycle',
  onOnline: 'app:lifecycle', onOffline: 'app:lifecycle',
  // Account Security
  onPasswordChange: 'security:read', onTotpChange: 'security:read',
  onAppPasswordChange: 'security:read', onEncryptionChange: 'security:read',
  onDisplayNameChange: 'security:read',
  // Sidebar Apps
  onSidebarAppOpen: 'ui:observe', onSidebarAppClose: 'ui:observe',
  onSidebarAppChange: 'ui:observe',
  // Avatar
  onAvatarResolve: 'email:read',
  // Router
  onNavigate: 'ui:observe', onRouteEnter: 'ui:observe', onRouteLeave: 'ui:observe',
};

// Map hook names â†’ actual HookBus instances
const HOOK_BUSES: Record<string, { register: (pluginId: string, handler: (...args: unknown[]) => unknown, order?: number) => Disposable }> = {
  // Email
  ...Object.fromEntries(Object.entries(emailHooks)),
  // Calendar
  ...Object.fromEntries(Object.entries(calendarHooks)),
  // Calendar Form
  ...Object.fromEntries(Object.entries(calendarFormHooks)),
  // Contacts
  ...Object.fromEntries(Object.entries(contactHooks)),
  // Files
  ...Object.fromEntries(Object.entries(fileHooks)),
  // Auth
  ...Object.fromEntries(Object.entries(authHooks)),
  // Settings
  ...Object.fromEntries(Object.entries(settingsHooks)),
  // Identity
  ...Object.fromEntries(Object.entries(identityHooks)),
  // Filters
  ...Object.fromEntries(Object.entries(filterHooks)),
  // Tasks
  ...Object.fromEntries(Object.entries(taskHooks)),
  // Templates
  ...Object.fromEntries(Object.entries(templateHooks)),
  // S/MIME
  ...Object.fromEntries(Object.entries(smimeHooks)),
  // Vacation
  ...Object.fromEntries(Object.entries(vacationHooks)),
  // UI
  ...Object.fromEntries(Object.entries(uiHooks)),
  // Theme
  ...Object.fromEntries(Object.entries(themeHooks)),
  // Toast
  ...Object.fromEntries(Object.entries(toastHooks)),
  // Drag & Drop
  ...Object.fromEntries(Object.entries(dragDropHooks)),
  // Keyboard
  ...Object.fromEntries(Object.entries(keyboardHooks)),
  // App Lifecycle
  ...Object.fromEntries(Object.entries(appLifecycleHooks)),
  // Account Security
  ...Object.fromEntries(Object.entries(accountSecurityHooks)),
  // Sidebar Apps
  ...Object.fromEntries(Object.entries(sidebarAppHooks)),
  // Avatar
  ...Object.fromEntries(Object.entries(avatarHooks)),
  // Render
  ...Object.fromEntries(Object.entries(renderHooks)),
  // Router
  ...Object.fromEntries(Object.entries(routerHooks)),
};

// --- Slot registration bridge --------------------------------
// Lazy import to avoid circular dependency â€” plugin-store imports plugin-api indirectly

let registerSlotFn: ((name: SlotName, reg: { pluginId: string; component: React.ComponentType<Record<string, unknown>>; order: number }) => Disposable) | null = null;

export function setSlotRegistrationBridge(fn: typeof registerSlotFn): void {
  registerSlotFn = fn;
}

function registerSlot(
  pluginId: string,
  slotName: SlotName,
  component: React.ComponentType<Record<string, unknown>>,
  order: number = 100,
): Disposable {
  if (!registerSlotFn) {
    console.warn(`[plugin:${pluginId}] Slot registration not available yet`);
    return { dispose: () => {} };
  }
  return registerSlotFn(slotName, { pluginId, component, order });
}

// --- Factory -------------------------------------------------

export function createPluginAPI(plugin: InstalledPlugin): PluginAPI {
  // Build hooks proxy â€” each hook method checks permission and registers on the right bus
  const hooks: PluginHooksAPI = {} as PluginHooksAPI;

  for (const [hookName, bus] of Object.entries(HOOK_BUSES)) {
    const perm = HOOK_PERMISSIONS[hookName];
    if (!perm) continue;

    if (hookName === 'onInterval') {
      // Special: onInterval takes (handler, intervalMs)
      (hooks as unknown as Record<string, unknown>)[hookName] = (handler: () => void, intervalMs: number) => {
        if (!hasPermission(plugin, perm)) return { dispose: () => {} };
        const safeMs = Math.max(intervalMs, 60_000); // min 60s
        const id = setInterval(handler, safeMs);
        return { dispose: () => clearInterval(id) };
      };
    } else if (hookName === 'registerShortcut') {
      // Special: registerShortcut takes a KeyboardShortcut object
      (hooks as unknown as Record<string, unknown>)[hookName] = (shortcut: KeyboardShortcut) => {
        return guardedHook(plugin, perm, bus, shortcut.handler);
      };
    } else {
      (hooks as unknown as Record<string, unknown>)[hookName] = (handler: (...args: unknown[]) => unknown) => {
        return guardedHook(plugin, perm, bus, handler);
      };
    }
  }

  return {
    plugin: {
      id: plugin.id,
      version: plugin.version,
      settings: { ...plugin.settings },
    },

    i18n: createPluginI18n(plugin.id),

    ui: {
      registerToolbarAction: (action: ToolbarAction) => {
        requirePermission(plugin, 'ui:toolbar');
        const Component = () => {
          const externals = getPluginExternals();
          const React = externals?.React;
          if (!React) return null;
          const createElement = (React as { createElement: typeof import('react').createElement }).createElement;
          return createElement('button', {
            onClick: action.onClick,
            className: 'plugin-toolbar-action',
            title: action.label,
          }, action.label);
        };
        return registerSlot(plugin.id, 'toolbar-actions', Component as React.ComponentType<Record<string, unknown>>, action.order ?? 100);
      },

      registerEmailBanner: (factory: BannerFactory) => {
        requirePermission(plugin, 'ui:email-banner');
        return registerSlot(plugin.id, 'email-banner', factory.render as unknown as React.ComponentType<Record<string, unknown>>, 100);
      },

      registerEmailFooter: (component: React.ComponentType) => {
        requirePermission(plugin, 'ui:email-footer');
        return registerSlot(plugin.id, 'email-footer', component as React.ComponentType<Record<string, unknown>>, 100);
      },

      registerSettingsSection: (section: SettingsSection) => {
        requirePermission(plugin, 'ui:settings-section');
        return registerSlot(plugin.id, 'settings-section', section.render as React.ComponentType<Record<string, unknown>>, 100);
      },

      registerComposerAction: (action: ComposerAction) => {
        requirePermission(plugin, 'ui:composer-toolbar');
        const Component = () => {
          const externals = getPluginExternals();
          const React = externals?.React;
          if (!React) return null;
          const createElement = (React as { createElement: typeof import('react').createElement }).createElement;
          return createElement('button', {
            onClick: action.onClick,
            className: 'plugin-composer-action',
            title: action.label,
          }, action.label);
        };
        return registerSlot(plugin.id, 'composer-toolbar', Component as React.ComponentType<Record<string, unknown>>, action.order ?? 100);
      },

      registerSidebarWidget: (widget: SidebarWidget) => {
        requirePermission(plugin, 'ui:sidebar-widget');
        return registerSlot(plugin.id, 'sidebar-widget', widget.render as React.ComponentType<Record<string, unknown>>, widget.order ?? 100);
      },

      registerComposerSidebar: (widget: SidebarWidget) => {
        requirePermission(plugin, 'ui:composer-sidebar');
        const slot = widget.side === 'right' ? 'composer-sidebar-right' : 'composer-sidebar';
        return registerSlot(plugin.id, slot, widget.render as React.ComponentType<Record<string, unknown>>, widget.order ?? 100);
      },

      registerDetailSidebar: (widget: SidebarWidget) => {
        requirePermission(plugin, 'ui:sidebar-widget');
        return registerSlot(plugin.id, 'email-detail-sidebar', widget.render as React.ComponentType<Record<string, unknown>>, widget.order ?? 100);
      },

      registerContextMenuItem: (item: ContextMenuItem) => {
        requirePermission(plugin, 'ui:context-menu');
        const Component = () => {
          const externals = getPluginExternals();
          const React = externals?.React;
          if (!React) return null;
          const createElement = (React as { createElement: typeof import('react').createElement }).createElement;
          return createElement('button', {
            onClick: () => item.onClick([]),
            className: 'plugin-context-menu-item',
          }, item.label);
        };
        return registerSlot(plugin.id, 'context-menu-email', Component as React.ComponentType<Record<string, unknown>>, item.order ?? 100);
      },

      registerNavigationRailItem: (component: React.ComponentType) => {
        requirePermission(plugin, 'ui:navigation-rail');
        return registerSlot(plugin.id, 'navigation-rail-bottom', component as React.ComponentType<Record<string, unknown>>, 100);
      },

      registerCalendarEventAction: (action: CalendarEventAction) => {
        requirePermission(plugin, 'ui:calendar-action');
        const Component = (props: Record<string, unknown>) => {
          const externals = getPluginExternals();
          const React = externals?.React;
          if (!React) return null;
          const createElement = (React as { createElement: typeof import('react').createElement }).createElement;
          const iconSpan = createElement('span', {
            'aria-hidden': 'true',
            style: { display: 'contents' },
            dangerouslySetInnerHTML: {
              __html: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m22 8-6 4 6 4V8z"/><rect x="2" y="8" width="14" height="12" rx="2"/></svg>',
            },
          });
          return createElement('button', {
            onClick: () => action.onClick(
              props.eventData as import('./plugin-types').CalendarEventFormView,
              { setVirtualLocation: props.setVirtualLocation as (url: string) => void },
            ),
            className: 'inline-flex items-center gap-1.5 h-9 px-3 text-sm font-medium rounded-md border border-input bg-background hover:bg-accent hover:text-accent-foreground transition-all duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background cursor-pointer',
            title: action.label,
            type: 'button',
          }, iconSpan, action.label);
        };
        return registerSlot(plugin.id, 'calendar-event-actions', Component as React.ComponentType<Record<string, unknown>>, action.order ?? 100);
      },

      registerAdminPage: (page: AdminPageSection) => {
        requirePermission(plugin, 'ui:admin-page');
        return registerSlot(plugin.id, 'admin-plugin-page', page.render as React.ComponentType<Record<string, unknown>>, 100);
      },
    },

    hooks,

    toast: {
      success: (message: string) => appToast.success(message),
      error: (message: string) => appToast.error(message),
      info: (message: string) => appToast.info(message),
      warning: (message: string) => appToast.warning(message),
    },

    http: {
      post: async (path: string, body: Record<string, unknown>) => {
        requirePermission(plugin, 'http:post');
        if (typeof path !== 'string' || !path.startsWith('/api/')) {
          throw new Error('path must start with /api/');
        }
        const url = new URL(path, globalThis.location.origin);
        if (url.origin !== globalThis.location.origin) {
          throw new Error('path must resolve to the same origin');
        }
        const { client } = useAuthStore.getState();
        const headers: Record<string, string> = { 'Content-Type': 'application/json' };
        if (client) {
          headers['Authorization'] = client.getAuthHeader();
          headers['X-JMAP-Username'] = client.getUsername();
        }
        const res = await fetch(url.pathname + url.search, {
          method: 'POST',
          headers,
          body: JSON.stringify(body),
        });
        const data = await res.json().catch(() => null);
        return { ok: res.ok, status: res.status, data };
      },

      fetch: async (rawUrl: string, init?: PluginFetchInit) => {
        requirePermission(plugin, 'http:fetch');
        if (typeof rawUrl !== 'string') {
          throw new Error('url must be a string');
        }
        let url: URL;
        try {
          url = new URL(rawUrl);
        } catch {
          throw new Error('url must be an absolute https:// URL');
        }
        const allowlist = plugin.httpOrigins ?? [];
        if (allowlist.length === 0) {
          throw new Error(`Plugin "${plugin.id}" has no httpOrigins declared`);
        }
        if (!originMatchesAllowlist(url, allowlist)) {
          throw new Error(`Origin ${url.origin} not in plugin httpOrigins allowlist`);
        }
        // Defence-in-depth: don't let the plugin smuggle a header that the
        // host's same-origin /api flow uses to authenticate the user.
        const safeHeaders: Record<string, string> = {};
        if (init?.headers) {
          for (const [k, v] of Object.entries(init.headers)) {
            const lower = k.toLowerCase();
            if (lower === 'cookie' || lower === 'x-jmap-username') continue;
            safeHeaders[k] = v;
          }
        }
        const res = await fetch(url.toString(), {
          method: init?.method ?? 'GET',
          headers: safeHeaders,
          // eslint-disable-next-line no-undef
          body: (init?.body ?? undefined) as BodyInit | undefined,
          signal: init?.signal,
          credentials: 'omit',
          mode: 'cors',
          redirect: 'follow',
        });
        const headersOut: Record<string, string> = {};
        res.headers.forEach((value, key) => { headersOut[key.toLowerCase()] = value; });
        return {
          ok: res.ok,
          status: res.status,
          statusText: res.statusText,
          headers: headersOut,
          text: () => res.text(),
          json: () => res.json().catch(() => null),
          arrayBuffer: () => res.arrayBuffer(),
          blob: () => res.blob(),
        };
      },
    },

    storage: createPluginStorage(plugin.id),
    log: createPluginLogger(plugin.id),

    admin: {
      getConfig: async (key: string) => {
        requirePermission(plugin, 'admin:config');
        const res = await apiFetch(`/api/admin/plugins/${encodeURIComponent(plugin.id)}/config`);
        if (!res.ok) return null;
        const data = await res.json();
        return data[key] ?? null;
      },
      getAllConfig: async () => {
        requirePermission(plugin, 'admin:config');
        const res = await apiFetch(`/api/admin/plugins/${encodeURIComponent(plugin.id)}/config`);
        if (!res.ok) return {};
        return res.json();
      },
      setConfig: async (key: string, value: unknown) => {
        requirePermission(plugin, 'admin:config');
        await apiFetch(`/api/admin/plugins/${encodeURIComponent(plugin.id)}/config`, {
          method: 'PUT',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ key, value }),
        });
      },
      deleteConfig: async (key: string) => {
        requirePermission(plugin, 'admin:config');
        await apiFetch(`/api/admin/plugins/${encodeURIComponent(plugin.id)}/config`, {
          method: 'DELETE',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ key }),
        });
      },
    },
  };
}
