/**
 * @fileoverview Frontend API client for the Paperclip plugin system.
 *
 * All functions in `pluginsApi` map 1:1 to REST endpoints on
 * `server/src/routes/plugins.ts`. Call sites should consume these functions
 * through React Query hooks (`useQuery` / `useMutation`) and reference cache
 * keys from `queryKeys.plugins.*`.
 *
 * @see ui/src/lib/queryKeys.ts for cache key definitions.
 * @see server/src/routes/plugins.ts for endpoint implementation details.
 */

import type {
  PluginLauncherDeclaration,
  PluginLauncherRenderContextSnapshot,
  PluginUiSlotDeclaration,
  PluginRecord,
  PluginConfig,
  PluginStatus,
} from "@paperclipai/shared";
import { api } from "./client";

/**
 * Normalized UI contribution record returned by `GET /api/plugins/ui-contributions`.
 *
 * Only populated for plugins in `ready` state that declare at least one UI slot
 * or launcher. The `slots` array is sourced from `manifest.ui.slots`. The
 * `launchers` array aggregates both legacy `manifest.launchers` and
 * `manifest.ui.launchers`.
 */
export type PluginUiContribution = {
  pluginId: string;
  pluginKey: string;
  displayName: string;
  version: string;
  updatedAt?: string;
  /**
   * Relative filename of the UI entry module within the plugin's UI directory.
   * The host constructs the full import URL as
   * `/_plugins/${pluginId}/ui/${uiEntryFile}`.
   */
  uiEntryFile: string;
  slots: PluginUiSlotDeclaration[];
  launchers: PluginLauncherDeclaration[];
};

/**
 * Health check result returned by `GET /api/plugins/:pluginId/health`.
 *
 * The `healthy` flag summarises whether all checks passed. Individual check
 * results are available in `checks` for detailed diagnostics display.
 */
export interface PluginHealthCheckResult {
  pluginId: string;
  /** The plugin's current lifecycle status at time of check. */
  status: string;
  /** True if all health checks passed. */
  healthy: boolean;
  /** Individual diagnostic check results. */
  checks: Array<{
    name: string;
    passed: boolean;
    /** Human-readable description of a failure, if any. */
    message?: string;
  }>;
  /** The most recent error message if the plugin is in `error` state. */
  lastError?: string;
}

/**
 * Worker diagnostics returned as part of the dashboard response.
 */
export interface PluginWorkerDiagnostics {
  status: string;
  pid: number | null;
  uptime: number | null;
  consecutiveCrashes: number;
  totalCrashes: number;
  pendingRequests: number;
  lastCrashAt: number | null;
  nextRestartAt: number | null;
}

/**
 * A recent job run entry returned in the dashboard response.
 */
export interface PluginDashboardJobRun {
  id: string;
  jobId: string;
  jobKey?: string;
  trigger: string;
  status: string;
  durationMs: number | null;
  error: string | null;
  startedAt: string | null;
  finishedAt: string | null;
  createdAt: string;
}

/**
 * A recent webhook delivery entry returned in the dashboard response.
 */
export interface PluginDashboardWebhookDelivery {
  id: string;
  webhookKey: string;
  status: string;
  durationMs: number | null;
  error: string | null;
  startedAt: string | null;
  finishedAt: string | null;
  createdAt: string;
}

/**
 * Aggregated health dashboard data returned by `GET /api/plugins/:pluginId/dashboard`.
 *
 * Contains worker diagnostics, recent job runs, recent webhook deliveries,
 * and the current health check result — all in a single response.
 */
export interface PluginDashboardData {
  pluginId: string;
  /** Worker process diagnostics, or null if no worker is registered. */
  worker: PluginWorkerDiagnostics | null;
  /** Recent job execution history (newest first, max 10). */
  recentJobRuns: PluginDashboardJobRun[];
  /** Recent inbound webhook deliveries (newest first, max 10). */
  recentWebhookDeliveries: PluginDashboardWebhookDelivery[];
  /** Current health check results. */
  health: PluginHealthCheckResult;
  /** ISO 8601 timestamp when the dashboard data was generated. */
  checkedAt: string;
}

export interface AvailablePluginExample {
  packageName: string;
  pluginKey: string;
  displayName: string;
  description: string;
  localPath: string;
  tag: "example";
}

/**
 * Plugin management API client.
 *
 * All methods are thin wrappers around the `api` base client. They return
 * promises that resolve to typed JSON responses or throw on HTTP errors.
 *
 * @example
 * ```tsx
 * // In a component:
 * const { data: plugins } = useQuery({
 *   queryKey: queryKeys.plugins.all,
 *   queryFn: () => pluginsApi.list(),
 * });
 * ```
 */
export const pluginsApi = {
  /**
   * List all installed plugins, optionally filtered by lifecycle status.
   *
   * @param status - Optional filter; must be a valid `PluginStatus` value.
   *   Invalid values are rejected by the server with HTTP 400.
   */
  list: (status?: PluginStatus) =>
    api.get<PluginRecord[]>(`/plugins${status ? `?status=${status}` : ""}`),

  /**
   * List bundled example plugins available from the current repo checkout.
   */
  listExamples: () =>
    api.get<AvailablePluginExample[]>("/plugins/examples"),

  /**
   * Fetch a single plugin record by its UUID or plugin key.
   *
   * @param pluginId - The plugin's UUID (from `PluginRecord.id`) or plugin key.
   */
  get: (pluginId: string) =>
    api.get<PluginRecord>(`/plugins/${pluginId}`),

  /**
   * Install a plugin from npm or a local path.
   *
   * On success, the plugin is registered in the database and transitioned to
   * `ready` state. The response is the newly created `PluginRecord`.
   *
   * @param params.packageName - npm package name (e.g. `@paperclip/plugin-linear`)
   *   or a filesystem path when `isLocalPath` is `true`.
   * @param params.version - Target npm version tag/range (optional; defaults to latest).
   * @param params.isLocalPath - Set to `true` when `packageName` is a local path.
   */
  install: (params: { packageName: string; version?: string; isLocalPath?: boolean }) =>
    api.post<PluginRecord>("/plugins/install", params),

  /**
   * Uninstall a plugin.
   *
   * @param pluginId - UUID of the plugin to remove.
   * @param purge - If `true`, permanently delete all plugin data (hard delete).
   *   Otherwise the plugin is soft-deleted with a 30-day data retention window.
   */
  uninstall: (pluginId: string, purge?: boolean) =>
    api.delete<{ ok: boolean }>(`/plugins/${pluginId}${purge ? "?purge=true" : ""}`),

  /**
   * Transition a plugin from `error` state back to `ready`.
   * No-ops if the plugin is already enabled.
   *
   * @param pluginId - UUID of the plugin to enable.
   */
  enable: (pluginId: string) =>
    api.post<{ ok: boolean }>(`/plugins/${pluginId}/enable`, {}),

  /**
   * Disable a plugin (transition to `error` state with an operator sentinel).
   * The plugin's worker is stopped; it will not process events until re-enabled.
   *
   * @param pluginId - UUID of the plugin to disable.
   * @param reason - Optional human-readable reason stored in `lastError`.
   */
  disable: (pluginId: string, reason?: string) =>
    api.post<{ ok: boolean }>(`/plugins/${pluginId}/disable`, reason ? { reason } : {}),

  /**
   * Run health diagnostics for a plugin.
   *
   * Only meaningful for plugins in `ready` state. Returns the result of all
   * registered health checks. Called on a 30-second polling interval by
   * {@link PluginSettings}.
   *
   * @param pluginId - UUID of the plugin to health-check.
   */
  health: (pluginId: string) =>
    api.get<PluginHealthCheckResult>(`/plugins/${pluginId}/health`),

  /**
   * Fetch aggregated health dashboard data for a plugin.
   *
   * Returns worker diagnostics, recent job runs, recent webhook deliveries,
   * and the current health check result in a single request. Used by the
   * {@link PluginSettings} page to render the runtime dashboard section.
   *
   * @param pluginId - UUID of the plugin.
   */
  dashboard: (pluginId: string) =>
    api.get<PluginDashboardData>(`/plugins/${pluginId}/dashboard`),

  /**
   * Fetch recent log entries for a plugin.
   *
   * @param pluginId - UUID of the plugin.
   * @param options - Optional filters: limit, level, since.
   */
  logs: (pluginId: string, options?: { limit?: number; level?: string; since?: string }) => {
    const params = new URLSearchParams();
    if (options?.limit) params.set("limit", String(options.limit));
    if (options?.level) params.set("level", options.level);
    if (options?.since) params.set("since", options.since);
    const qs = params.toString();
    return api.get<Array<{ id: string; pluginId: string; level: string; message: string; meta: Record<string, unknown> | null; createdAt: string }>>(
      `/plugins/${pluginId}/logs${qs ? `?${qs}` : ""}`,
    );
  },

  /**
   * Upgrade a plugin to a newer version.
   *
   * If the new version declares additional capabilities, the plugin is
   * transitioned to `upgrade_pending` state awaiting operator approval.
   *
   * @param pluginId - UUID of the plugin to upgrade.
   * @param version - Target version (optional; defaults to latest published).
   */
  upgrade: (pluginId: string, version?: string) =>
    api.post<{ ok: boolean }>(`/plugins/${pluginId}/upgrade`, version ? { version } : {}),

  /**
   * Returns normalized UI contribution declarations for ready plugins.
   * Used by the slot host runtime and launcher discovery surfaces.
   *
   * Response shape:
   * - `slots`: concrete React mount declarations from `manifest.ui.slots`
   * - `launchers`: host-owned entry points from `manifest.ui.launchers` plus
   *   the legacy top-level `manifest.launchers`
   *
   * @example
   * ```ts
   * const rows = await pluginsApi.listUiContributions();
   * const toolbarLaunchers = rows.flatMap((row) =>
   *   row.launchers.filter((launcher) => launcher.placementZone === "toolbarButton"),
   * );
   * ```
   */
  listUiContributions: () =>
    api.get<PluginUiContribution[]>("/plugins/ui-contributions"),

  // ===========================================================================
  // Plugin configuration endpoints
  // ===========================================================================

  /**
   * Fetch the current configuration for a plugin.
   *
   * Returns the `PluginConfig` record if one exists, or `null` if the plugin
   * has not yet been configured.
   *
   * @param pluginId - UUID of the plugin.
   */
  getConfig: (pluginId: string) =>
    api.get<PluginConfig | null>(`/plugins/${pluginId}/config`),

  /**
   * Save (create or update) the configuration for a plugin.
   *
   * The server validates `configJson` against the plugin's `instanceConfigSchema`
   * and returns the persisted `PluginConfig` record on success.
   *
   * @param pluginId - UUID of the plugin.
   * @param configJson - Configuration values matching the plugin's `instanceConfigSchema`.
   */
  saveConfig: (pluginId: string, configJson: Record<string, unknown>) =>
    api.post<PluginConfig>(`/plugins/${pluginId}/config`, { configJson }),

  /**
   * Call the plugin's `validateConfig` RPC method to test the configuration
   * without persisting it.
   *
   * Returns `{ valid: true }` on success, or `{ valid: false, message: string }`
   * when the plugin reports a validation failure.
   *
   * Only available when the plugin declares a `validateConfig` RPC handler.
   *
   * @param pluginId - UUID of the plugin.
   * @param configJson - Configuration values to validate.
   */
  testConfig: (pluginId: string, configJson: Record<string, unknown>) =>
    api.post<{ valid: boolean; message?: string }>(`/plugins/${pluginId}/config/test`, { configJson }),

  // ===========================================================================
  // Bridge proxy endpoints — used by the plugin UI bridge runtime
  // ===========================================================================

  /**
   * Proxy a `getData` call from a plugin UI component to its worker backend.
   *
   * This is the HTTP transport for `usePluginData(key, params)`. The bridge
   * runtime calls this method and maps the response into `PluginDataResult<T>`.
   *
   * On success, the response is `{ data: T }`.
   * On failure, the response body is a `PluginBridgeError`-shaped object
   * with `code`, `message`, and optional `details`.
   *
   * @param pluginId - UUID of the plugin whose worker should handle the request
   * @param key - Plugin-defined data key (e.g. `"sync-health"`)
   * @param params - Optional query parameters forwarded to the worker handler
   * @param companyId - Optional company scope used for board/company access checks.
   * @param renderEnvironment - Optional launcher/page snapshot forwarded for
   *   launcher-backed UI so workers can distinguish modal, drawer, popover, and
   *   page execution.
   *
   * Error responses:
   * - `401`/`403` when auth or company access checks fail
   * - `404` when the plugin or handler key does not exist
   * - `409` when the plugin is not in a callable runtime state
   * - `5xx` with a `PluginBridgeError`-shaped body when the worker throws
   *
   * @see PLUGIN_SPEC.md §13.8 — `getData`
   * @see PLUGIN_SPEC.md §19.7 — Error Propagation Through The Bridge
   */
  bridgeGetData: (
    pluginId: string,
    key: string,
    params?: Record<string, unknown>,
    companyId?: string | null,
    renderEnvironment?: PluginLauncherRenderContextSnapshot | null,
  ) =>
    api.post<{ data: unknown }>(`/plugins/${pluginId}/data/${encodeURIComponent(key)}`, {
      companyId: companyId ?? undefined,
      params,
      renderEnvironment: renderEnvironment ?? undefined,
    }),

  /**
   * Proxy a `performAction` call from a plugin UI component to its worker backend.
   *
   * This is the HTTP transport for `usePluginAction(key)`. The bridge runtime
   * calls this method when the action function is invoked.
   *
   * On success, the response is `{ data: T }`.
   * On failure, the response body is a `PluginBridgeError`-shaped object
   * with `code`, `message`, and optional `details`.
   *
   * @param pluginId - UUID of the plugin whose worker should handle the request
   * @param key - Plugin-defined action key (e.g. `"resync"`)
   * @param params - Optional parameters forwarded to the worker handler
   * @param companyId - Optional company scope used for board/company access checks.
   * @param renderEnvironment - Optional launcher/page snapshot forwarded for
   *   launcher-backed UI so workers can distinguish modal, drawer, popover, and
   *   page execution.
   *
   * Error responses:
   * - `401`/`403` when auth or company access checks fail
   * - `404` when the plugin or handler key does not exist
   * - `409` when the plugin is not in a callable runtime state
   * - `5xx` with a `PluginBridgeError`-shaped body when the worker throws
   *
   * @see PLUGIN_SPEC.md §13.9 — `performAction`
   * @see PLUGIN_SPEC.md §19.7 — Error Propagation Through The Bridge
   */
  bridgePerformAction: (
    pluginId: string,
    key: string,
    params?: Record<string, unknown>,
    companyId?: string | null,
    renderEnvironment?: PluginLauncherRenderContextSnapshot | null,
  ) =>
    api.post<{ data: unknown }>(`/plugins/${pluginId}/actions/${encodeURIComponent(key)}`, {
      companyId: companyId ?? undefined,
      params,
      renderEnvironment: renderEnvironment ?? undefined,
    }),
};
