// Plugin store - manages installed plugins and lifecycle. Slot registrations
// are owned by `lib/plugin-sandbox/registry` (per-iframe), not by the store.

import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import type { InstalledPlugin, PluginStatus } from '@/lib/plugin-types';
import { pluginStorage } from '@/lib/plugin-storage';
import { extractPlugin } from '@/lib/plugin-validator';
import { loadPlugin, deactivatePlugin, setPluginStoreAccessor, setupAutoDisable } from '@/lib/plugin-loader';
import { removeAllPluginHooks } from '@/lib/plugin-hooks';
import { requestConsent } from '@/lib/plugin-sandbox/consent';
import { sha256Hex } from '@/lib/plugin-sandbox/bundle-integrity';
import { verifySignature } from '@/lib/plugin-sandbox/bundle-signing';
import { usePolicyStore } from '@/stores/policy-store';
import { apiFetch } from '@/lib/browser-navigation';
import { IMPLICIT_PERMISSIONS } from '@/lib/plugin-types';
import type { Permission } from '@/lib/plugin-types';

let pluginInitializationPromise: Promise<void> | null = null;

// ─── Store Interface ─────────────────────────────────────────

interface PluginStoreState {
  plugins: InstalledPlugin[];
  initialized: boolean;

  // Management
  installPlugin: (file: File) => Promise<{ success: boolean; error?: string; warnings?: string[] }>;
  uninstallPlugin: (id: string) => void;
  enablePlugin: (id: string) => Promise<void>;
  disablePlugin: (id: string) => void;
  updatePluginSettings: (id: string, settings: Record<string, unknown>) => void;

  // Runtime (called by plugin loader)
  setPluginStatus: (id: string, status: PluginStatus, error?: string) => void;

  // Init
  initializePlugins: () => Promise<void>;
}

// ─── Store ───────────────────────────────────────────────────

export const usePluginStore = create<PluginStoreState>()(
  persist(
    (set, get) => ({
      plugins: [],
      initialized: false,

      installPlugin: async (file: File) => {
        const result = await extractPlugin(file);
        if (!result.valid || !result.manifest) {
          return { success: false, error: result.errors.join('; '), warnings: result.warnings };
        }

        const { manifest, code } = result;
        const { plugins } = get();

        // Check for duplicate
        const existing = plugins.find(p => p.id === manifest.id);
        if (existing) {
          // Update: deactivate old, replace
          deactivatePlugin(manifest.id);
        }

        // Compute bundleHash so the admin-approval gate can pin to this
        // specific bundle (server-side state keys on (id, hash) pairs).
        const bundleHash = await sha256Hex(code).catch(() => undefined);

        const plugin: InstalledPlugin = {
          id: manifest.id,
          name: manifest.name,
          version: manifest.version,
          author: manifest.author,
          description: manifest.description,
          type: manifest.type,
          permissions: manifest.permissions,
          entrypoint: manifest.entrypoint,
          enabled: false, // Start disabled, user must enable
          status: 'installed',
          managed: false,
          forceEnabled: false,
          adminApproved: false, // Requires admin approval before it can be enabled
          settings: existing?.settings ?? {},
          settingsSchema: manifest.settingsSchema,
          ...(bundleHash ? { bundleHash } : {}),
          ...(manifest.httpOrigins && manifest.httpOrigins.length > 0
            ? { httpOrigins: manifest.httpOrigins }
            : {}),
          ...(manifest.apiPostPaths && manifest.apiPostPaths.length > 0
            ? { apiPostPaths: manifest.apiPostPaths }
            : {}),
        };

        // Save code to IndexedDB
        await pluginStorage.saveCode(manifest.id, code);

        if (existing) {
          set({ plugins: plugins.map(p => p.id === manifest.id ? plugin : p) });
        } else {
          set({ plugins: [...plugins, plugin] });
        }

        return { success: true, warnings: result.warnings };
      },

      uninstallPlugin: (id: string) => {
        const { plugins } = get();
        const plugin = plugins.find(p => p.id === id);
        if (!plugin) return;
        const forceEnabledByPolicy = usePolicyStore.getState().isPluginForceEnabled(id);
        if (plugin.forceEnabled || forceEnabledByPolicy) return;

        // Deactivate if running
        deactivatePlugin(id);
        removeAllPluginHooks(id);

        // Clean up storage
        pluginStorage.deleteCode(id);
        pluginStorage.deletePreview(id);

        // Remove plugin-scoped localStorage entries
        if (typeof window !== 'undefined') {
          const prefix = `plugin:${id}:`;
          const keysToRemove: string[] = [];
          for (let i = 0; i < localStorage.length; i++) {
            const key = localStorage.key(i);
            if (key?.startsWith(prefix)) keysToRemove.push(key);
          }
          keysToRemove.forEach(k => localStorage.removeItem(k));
        }

        set({ plugins: plugins.filter(p => p.id !== id) });
      },

      enablePlugin: async (id: string) => {
        const { plugins } = get();
        const plugin = plugins.find(p => p.id === id);
        if (!plugin) return;

        // Admin approval gate. Managed (admin-pushed) plugins are pre-
        // approved. For user-installed plugins the server-side state is
        // authoritative: the client-only `isPluginApproved` flag is kept as
        // a fast-path hint but the server result wins.
        const requireApproval = usePolicyStore.getState().isFeatureEnabled('requirePluginApproval');
        const policyApproved = plugin.adminApproved || plugin.managed || usePolicyStore.getState().isPluginApproved(id);
        if (requireApproval && !policyApproved && plugin.bundleHash) {
          const status = await checkServerApproval(plugin.id, plugin.bundleHash).catch(() => null);
          if (status?.status === 'approved') {
            // Approval available; proceed.
          } else if (status?.status === 'denied') {
            set(state => ({
              plugins: state.plugins.map(p =>
                p.id === id ? { ...p, status: 'error' as PluginStatus, error: 'Plugin denied by administrator' } : p
              ),
            }));
            return;
          } else {
            // 'pending' or 'not-requested' — submit a request and refuse to enable.
            await submitApprovalRequest(plugin).catch(() => { /* best effort */ });
            set(state => ({
              plugins: state.plugins.map(p =>
                p.id === id ? { ...p, status: 'error' as PluginStatus, error: 'Awaiting administrator approval' } : p
              ),
            }));
            return;
          }
        } else if (requireApproval && !policyApproved) {
          // No bundleHash means we can't pin the approval — refuse.
          return;
        }

        // Per-user consent gate: prompt for any permission the user has not
        // explicitly approved yet. Managed plugins (admin-pushed) skip this —
        // the admin has already approved them at install time.
        const implicit = new Set<string>(IMPLICIT_PERMISSIONS);
        const granted = new Set<string>(plugin.grantedPermissions ?? []);
        const missing = (plugin.permissions ?? [])
          .filter((p): p is Permission => !!p)
          .filter((p) => !implicit.has(p) && !granted.has(p));
        if (missing.length > 0 && !plugin.managed) {
          const accepted = await requestConsent(plugin.id, plugin.name, missing as Permission[]);
          if (!accepted) return;
          // Persist the grants so future enables don't re-prompt.
          const allGranted = [...new Set<string>([...granted, ...missing])];
          set(state => ({
            plugins: state.plugins.map(p =>
              p.id === id ? { ...p, grantedPermissions: allGranted } : p
            ),
          }));
        }

        // Ensure bridge is wired before loading (may not have run initializePlugins yet)
        setPluginStoreAccessor({ setPluginStatus: get().setPluginStatus });

        set(state => ({
          plugins: state.plugins.map(p =>
            p.id === id ? { ...p, enabled: true, status: 'enabled' as PluginStatus, error: undefined } : p
          ),
        }));

        // Load it immediately
        const updatedPlugin = get().plugins.find(p => p.id === id);
        if (updatedPlugin) {
          await loadPlugin(updatedPlugin);
        }
      },

      disablePlugin: (id: string) => {
        const { plugins } = get();
        const plugin = plugins.find(p => p.id === id);
        if (!plugin) return;
        const forceEnabledByPolicy = usePolicyStore.getState().isPluginForceEnabled(id);
        if (plugin.forceEnabled || forceEnabledByPolicy) return;

        deactivatePlugin(id);

        set({
          plugins: plugins.map(p =>
            p.id === id ? { ...p, enabled: false, status: 'disabled' as PluginStatus, error: undefined } : p
          ),
        });
      },

      updatePluginSettings: (id: string, settings: Record<string, unknown>) => {
        const { plugins } = get();
        set({
          plugins: plugins.map(p =>
            p.id === id ? { ...p, settings: { ...p.settings, ...settings } } : p
          ),
        });
      },

      setPluginStatus: (id: string, status: PluginStatus, error?: string) => {
        set(state => ({
          plugins: state.plugins.map(p =>
            p.id === id ? { ...p, status, error } : p
          ),
        }));
      },

      initializePlugins: async () => {
        if (get().initialized) return;

        if (pluginInitializationPromise) {
          await pluginInitializationPromise;
          return;
        }

        pluginInitializationPromise = (async () => {
          // Clean up any previously persisted duplicates by plugin id.
          const deduped = dedupeInstalledPlugins(get().plugins);
          if (deduped.length !== get().plugins.length) {
            set({ plugins: deduped });
          }

          // Wire up bridges
          setPluginStoreAccessor({
            setPluginStatus: get().setPluginStatus,
          });
          setupAutoDisable();

          // Sync server-managed plugins before loading
          await syncServerPlugins(get, set);

          // Load all enabled plugins
          const enabledPlugins = get().plugins.filter(p => p.enabled && p.status !== 'error');
          for (const plugin of enabledPlugins) {
            await loadPlugin(plugin);
          }

          set({ initialized: true });
        })();

        try {
          await pluginInitializationPromise;
        } finally {
          pluginInitializationPromise = null;
        }
      },
    }),
    {
      name: 'plugin-storage',
      partialize: (state) => ({
        plugins: state.plugins.map(p => ({
          ...p,
          // Reset runtime state on persist
          status: p.enabled ? 'enabled' : 'installed',
          error: undefined,
        })),
      }),
      onRehydrateStorage: () => {
        return (state) => {
          if (state) {
            state.plugins = markServerManagedPlugins(state.plugins);
            state.plugins = dedupeInstalledPlugins(state.plugins);
            state.initialized = false;
          }
        };
      },
    }
  )
);

// ─── Server Plugin Sync ──────────────────────────────────────

interface ServerPluginInfo {
  id: string;
  name: string;
  version: string;
  author: string;
  description: string;
  type: string;
  permissions: string[];
  entrypoint: string;
  forceEnabled: boolean;
  /** Content hash of the bundle - changes whenever code changes, even if the version doesn't */
  bundleHash?: string;
  updatedAt?: string;
  /** True when the plugin was loaded from the server's PLUGIN_DEV_DIR */
  dev?: boolean;
  /** Allowlist of origins this plugin may target via api.http.fetch(). */
  httpOrigins?: string[];
  /** Allowlist of same-origin /api/* paths this plugin may target via api.http.post(). */
  apiPostPaths?: string[];
  /** Per-user settings schema, captured from the manifest server-side. */
  settingsSchema?: InstalledPlugin['settingsSchema'];
}

const SERVER_MANAGED_KEY = 'server-managed-plugin-ids';

function getServerManagedPluginIds(): Set<string> {
  try {
    const raw = localStorage.getItem(SERVER_MANAGED_KEY);
    return raw ? new Set(JSON.parse(raw)) : new Set();
  } catch { return new Set(); }
}

function setServerManagedPluginIds(ids: Set<string>): void {
  try {
    localStorage.setItem(SERVER_MANAGED_KEY, JSON.stringify([...ids]));
  } catch { /* ok */ }
}

function dedupeInstalledPlugins(plugins: InstalledPlugin[]): InstalledPlugin[] {
  const byId = new Map<string, InstalledPlugin>();

  for (const plugin of plugins) {
    const existing = byId.get(plugin.id);
    if (!existing) {
      byId.set(plugin.id, plugin);
      continue;
    }

    byId.set(plugin.id, {
      ...existing,
      ...plugin,
      enabled: existing.enabled || plugin.enabled,
      status: existing.enabled || plugin.enabled ? 'enabled' : plugin.status,
      settings: { ...existing.settings, ...plugin.settings },
      error: plugin.error ?? existing.error,
      managed: existing.managed || plugin.managed,
      forceEnabled: existing.forceEnabled || plugin.forceEnabled,
    });
  }

  return [...byId.values()];
}

function markServerManagedPlugins(plugins: InstalledPlugin[]): InstalledPlugin[] {
  const serverIds = getServerManagedPluginIds();
  if (serverIds.size === 0) return plugins;
  return plugins.map(plugin =>
    serverIds.has(plugin.id) ? { ...plugin, managed: true } : plugin
  );
}

/**
 * Sync server-managed plugins to the client.
 * Downloads missing plugin bundles and installs them into IndexedDB + store.
 * Force-enabled plugins are auto-enabled.
 * Plugins removed from the server are cleaned up from the client.
 */
async function syncServerPlugins(
  get: () => PluginStoreState,
  set: (partial: Partial<PluginStoreState> | ((state: PluginStoreState) => Partial<PluginStoreState>)) => void,
): Promise<void> {
  try {
    const res = await apiFetch('/api/plugins');
    if (!res.ok) return;

    const data: { plugins: ServerPluginInfo[] } = await res.json();
    if (!data.plugins || !Array.isArray(data.plugins)) return;

    const serverPlugins = data.plugins;
    const serverPluginIds = new Set(serverPlugins.map(p => p.id));

    // Track which plugins came from the server (so we can clean up stale ones)
    const prevServerIds = getServerManagedPluginIds();

    // Install or update server plugins that are missing/outdated locally
    for (const sp of serverPlugins) {
      const local = get().plugins.find(p => p.id === sp.id);

      if (!local) {
        // New server plugin - download and install
        const code = await downloadPluginBundle(sp.id, sp.bundleHash);
        if (!code) continue;

        await pluginStorage.saveCode(sp.id, code);

        const plugin: InstalledPlugin = {
          id: sp.id,
          name: sp.name,
          version: sp.version,
          author: sp.author,
          description: sp.description,
          type: sp.type as InstalledPlugin['type'],
          permissions: sp.permissions,
          entrypoint: sp.entrypoint,
          enabled: sp.forceEnabled,
          status: sp.forceEnabled ? 'enabled' : 'installed',
          managed: true,
          forceEnabled: sp.forceEnabled,
          adminApproved: true, // Server-managed plugins are always approved
          settings: {},
          settingsSchema: sp.settingsSchema,
          bundleHash: sp.bundleHash,
          ...(sp.httpOrigins && sp.httpOrigins.length > 0
            ? { httpOrigins: sp.httpOrigins }
            : {}),
          ...(sp.apiPostPaths && sp.apiPostPaths.length > 0
            ? { apiPostPaths: sp.apiPostPaths }
            : {}),
        };

        set(state => {
          if (state.plugins.some(p => p.id === sp.id)) {
            return {};
          }
          return { plugins: [...state.plugins, plugin] };
        });
      } else if (
        local.version !== sp.version ||
        // bundleHash mismatch covers re-uploads of the same version with new
        // code. Falsy local hash (older installs that never carried one) also
        // forces a refresh so we capture the hash on the next sync.
        (sp.bundleHash && local.bundleHash !== sp.bundleHash)
      ) {
        // Version or content changed - re-download bundle
        const code = await downloadPluginBundle(sp.id, sp.bundleHash);
        if (!code) continue;

        await pluginStorage.saveCode(sp.id, code);

        set(state => ({
          plugins: state.plugins.map(p =>
            p.id === sp.id
              ? {
                  ...p,
                  name: sp.name,
                  version: sp.version,
                  author: sp.author,
                  description: sp.description,
                  permissions: sp.permissions,
                  entrypoint: sp.entrypoint,
                  managed: true,
                  forceEnabled: sp.forceEnabled,
                  bundleHash: sp.bundleHash,
                  httpOrigins: sp.httpOrigins,
                  apiPostPaths: sp.apiPostPaths,
                  settingsSchema: sp.settingsSchema,
                }
              : p
          ),
        }));
      } else if (local.managed !== true || local.forceEnabled !== sp.forceEnabled) {
        set(state => ({
          plugins: state.plugins.map(p =>
            p.id === sp.id
              ? {
                  ...p,
                  managed: true,
                  forceEnabled: sp.forceEnabled,
                  settingsSchema: sp.settingsSchema,
                }
              : p
          ),
        }));
      } else if (
        JSON.stringify(local.settingsSchema ?? null) !== JSON.stringify(sp.settingsSchema ?? null)
      ) {
        // Schema drift: the bundle is current but the persisted plugin record
        // pre-dates the server passing settingsSchema through, so the per-user
        // settings UI was rendering empty. Patch the schema in place.
        set(state => ({
          plugins: state.plugins.map(p =>
            p.id === sp.id ? { ...p, settingsSchema: sp.settingsSchema } : p
          ),
        }));
      } else if (sp.forceEnabled && !local.enabled) {
        // Force-enable if the server says so but client has it disabled
        set(state => ({
          plugins: state.plugins.map(p =>
            p.id === sp.id
              ? { ...p, enabled: true, status: 'enabled' as const, managed: true, forceEnabled: true }
              : p
          ),
        }));
      }
    }

    // Ensure no duplicate IDs remain after sync.
    set(state => ({ plugins: dedupeInstalledPlugins(state.plugins) }));

    // Remove plugins that were previously server-managed but no longer on the server
    const staleIds = [...prevServerIds].filter(id => !serverPluginIds.has(id));
    if (staleIds.length > 0) {
      for (const id of staleIds) {
        deactivatePlugin(id);
        removeAllPluginHooks(id);
        pluginStorage.deleteCode(id);
      }
      const staleSet = new Set(staleIds);
      set(state => ({
        plugins: state.plugins.filter(p => !staleSet.has(p.id)),
      }));
    }

    // Persist current server plugin IDs for future cleanup
    setServerManagedPluginIds(serverPluginIds);
  } catch {
    // Sync failure is non-fatal - client continues with local plugins
    console.warn('[plugin-store] Server plugin sync failed, using local plugins only');
  }
}

async function downloadPluginBundle(pluginId: string, bundleHash?: string): Promise<string | null> {
  try {
    // Append the hash as a query string so any intermediary HTTP cache
    // (browser, service worker, CDN) treats each version as a distinct URL.
    const suffix = bundleHash ? `?v=${encodeURIComponent(bundleHash)}` : '';
    const res = await apiFetch(`/api/admin/plugins/${encodeURIComponent(pluginId)}/bundle${suffix}`);
    if (!res.ok) return null;
    const code = await res.text();
    // Ed25519 signature verification. Present on every server-managed bundle
    // since the signing module is server-side; refuse to persist a bundle
    // that fails verification. If the header is missing (older server / dev
    // build with signing disabled) we log and allow — the SHA-256 hash check
    // at load time still catches transport corruption.
    const sig = res.headers.get('X-Bundle-Signature');
    if (sig) {
      const ok = await verifySignature(code, sig);
      if (!ok) {
        console.error(`[plugin-store] Refusing bundle for "${pluginId}": signature verification failed`);
        return null;
      }
    } else {
      console.warn(`[plugin-store] Bundle for "${pluginId}" has no Ed25519 signature; loading without it`);
    }
    return code;
  } catch {
    console.warn(`[plugin-store] Failed to download bundle for plugin "${pluginId}"`);
    return null;
  }
}

// ─── Server-side admin-approval helpers ───────────────────────

async function checkServerApproval(pluginId: string, bundleHash: string): Promise<{ status: 'pending' | 'approved' | 'denied' | 'not-requested' } | null> {
  try {
    const url = `/api/plugin-approval-status?pluginId=${encodeURIComponent(pluginId)}&bundleHash=${encodeURIComponent(bundleHash)}`;
    const res = await apiFetch(url);
    if (!res.ok) return null;
    return await res.json();
  } catch {
    return null;
  }
}

async function submitApprovalRequest(plugin: InstalledPlugin): Promise<void> {
  if (!plugin.bundleHash) return;
  try {
    await apiFetch('/api/plugin-approval-status', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        pluginId: plugin.id,
        bundleHash: plugin.bundleHash,
        manifest: {
          name: plugin.name,
          version: plugin.version,
          author: plugin.author,
          description: plugin.description,
          permissions: plugin.permissions,
          httpOrigins: plugin.httpOrigins,
          apiPostPaths: plugin.apiPostPaths,
        },
      }),
    });
  } catch {
    /* best effort */
  }
}
