import type {
  Plugin,
  PluginContext,
  PluginCommand,
  MessageInterceptor,
  MessageContext,
  CustomRenderer,
  SettingsSchema,
  PluginTheme,
  PluginLogEntry,
} from './PluginInterfaces.js';
import type { IPluginStorage, IPluginEventClient } from './interfaces.js';
import { MemoryStorage } from './interfaces.js';
import { generateThemeCSS } from './theme-css.js';

/**
 * Configuration options for the PluginRegistry.
 */
export interface PluginRegistryOptions {
  /**
   * Storage adapter for persisting plugin settings.
   * Defaults to in-memory storage if not provided.
   * Pass `localStorage` (or a wrapper) for browser environments.
   */
  storage?: IPluginStorage;

  /**
   * Called when a plugin registers a theme.
   * The host is responsible for injecting the CSS into the document.
   * Receives the full theme ID (used as the CSS class name) and the generated CSS string.
   */
  onThemeRegistered?: (themeId: string, className: string, css: string) => void;

  /**
   * Called when a plugin unregisters a theme.
   * The host is responsible for removing the injected CSS.
   */
  onThemeUnregistered?: (themeId: string) => void;
}

/**
 * Central registry that manages all loaded plugins and their side-effects.
 * Create a single instance and share it across your application.
 */
export class PluginRegistry {
  private readonly plugins = new Map<string, { plugin: Plugin; context: PluginContext }>();
  private readonly commands = new Map<string, { pluginId: string; command: PluginCommand }>();
  private beforeSendInterceptors: Array<{ pluginId: string; interceptor: MessageInterceptor }> = [];
  private receiveInterceptors: Array<{ pluginId: string; interceptor: MessageInterceptor }> = [];
  private readonly renderers = new Map<string, { pluginId: string; renderer: CustomRenderer }>();
  private readonly settingsSchemas = new Map<string, SettingsSchema>();
  private readonly pluginSettings = new Map<string, Record<string, unknown>>();
  private readonly matrixHandlers = new Map<
    string,
    Array<{ pluginId: string; handler: (...args: any[]) => void }>
  >();
  private readonly timers = new Map<string, number[]>();
  private readonly themes = new Map<
    string,
    { pluginId: string; theme: PluginTheme }
  >();
  private logs: PluginLogEntry[] = [];
  private readonly maxLogs = 1000;
  private readonly storage: IPluginStorage;
  private readonly onThemeRegistered?: PluginRegistryOptions['onThemeRegistered'];
  private readonly onThemeUnregistered?: PluginRegistryOptions['onThemeUnregistered'];

  constructor(options: PluginRegistryOptions = {}) {
    this.storage = options.storage ?? new MemoryStorage();
    this.onThemeRegistered = options.onThemeRegistered;
    this.onThemeUnregistered = options.onThemeUnregistered;
  }

  // ---------------------------------------------------------------------------
  // Plugin lifecycle
  // ---------------------------------------------------------------------------

  /** Register a plugin and its context. Called after the host has loaded the module. */
  registerPlugin(id: string, plugin: Plugin, context: PluginContext): void {
    if (this.plugins.has(id)) {
      console.warn(`[PluginRegistry] Plugin ${id} is already registered`);
      return;
    }
    this.plugins.set(id, { plugin, context });
    this.timers.set(id, []);
  }

  /** Unregister a plugin, calling its onUnload hook and cleaning up all side-effects. */
  async unregisterPlugin(id: string): Promise<void> {
    const entry = this.plugins.get(id);
    if (!entry) return;

    if (entry.plugin.onUnload) {
      try {
        await entry.plugin.onUnload();
      } catch (err) {
        console.error(`[PluginRegistry] Error unloading plugin ${id}:`, err);
      }
    }

    for (const [cmdName, cmd] of this.commands.entries()) {
      if (cmd.pluginId === id) this.commands.delete(cmdName);
    }

    this.beforeSendInterceptors = this.beforeSendInterceptors.filter(i => i.pluginId !== id);
    this.receiveInterceptors = this.receiveInterceptors.filter(i => i.pluginId !== id);

    for (const [type, renderer] of this.renderers.entries()) {
      if (renderer.pluginId === id) this.renderers.delete(type);
    }

    for (const [themeId, theme] of this.themes.entries()) {
      if (theme.pluginId === id) {
        this.onThemeUnregistered?.(themeId);
        this.themes.delete(themeId);
      }
    }

    const timerIds = this.timers.get(id) ?? [];
    timerIds.forEach(timerId => clearInterval(timerId));
    this.timers.delete(id);

    for (const [eventType, handlers] of this.matrixHandlers.entries()) {
      const filtered = handlers.filter(h => h.pluginId !== id);
      if (filtered.length === 0) {
        this.matrixHandlers.delete(eventType);
      } else {
        this.matrixHandlers.set(eventType, filtered);
      }
    }

    this.plugins.delete(id);
  }

  // ---------------------------------------------------------------------------
  // Commands
  // ---------------------------------------------------------------------------

  /** Register a command contributed by a plugin. */
  registerCommand(pluginId: string, command: PluginCommand): void {
    if (this.commands.has(command.name)) {
      console.warn(`[PluginRegistry] Command /${command.name} already exists`);
      return;
    }
    this.commands.set(command.name, { pluginId, command });
  }

  /** Remove a registered command by name. */
  unregisterCommand(name: string): void {
    this.commands.delete(name);
  }

  /** Execute a command by name, passing raw argument string. */
  async executeCommand(name: string, rawArgs: string): Promise<string | void> {
    const cmd = this.commands.get(name);
    if (!cmd) throw new Error(`Command /${name} not found`);

    const args = this.parseCommandArgs(cmd.command, rawArgs);
    const entry = this.plugins.get(cmd.pluginId);
    if (!entry) throw new Error(`Plugin ${cmd.pluginId} not loaded`);

    return await cmd.command.run(args, entry.context);
  }

  private parseCommandArgs(command: PluginCommand, rawArgs: string): Record<string, unknown> {
    if (!command.args || command.args.length === 0) return {};
    const parts = rawArgs.trim().split(/\s+/);
    const result: Record<string, unknown> = {};
    command.args.forEach((arg, index) => {
      const argName = typeof arg === 'string' ? arg : arg.name;
      result[argName] = parts[index];
    });
    return result;
  }

  /** Return all registered commands. */
  getCommands(): Array<{ name: string; pluginId: string; command: PluginCommand }> {
    return Array.from(this.commands.entries()).map(([name, data]) => ({ name, ...data }));
  }

  // ---------------------------------------------------------------------------
  // Message interceptors
  // ---------------------------------------------------------------------------

  /** Add a before-send interceptor for a plugin. */
  addBeforeSendInterceptor(pluginId: string, interceptor: MessageInterceptor): void {
    this.beforeSendInterceptors.push({ pluginId, interceptor });
  }

  /** Add a receive interceptor for a plugin. */
  addReceiveInterceptor(pluginId: string, interceptor: MessageInterceptor): void {
    this.receiveInterceptors.push({ pluginId, interceptor });
  }

  /** Run a message through all before-send interceptors in registration order. */
  async processBeforeSend(msg: MessageContext): Promise<MessageContext> {
    let processed = { ...msg };
    for (const { interceptor } of this.beforeSendInterceptors) {
      try {
        await interceptor(processed);
      } catch (err) {
        console.error('[PluginRegistry] Error in beforeSend interceptor:', err);
      }
    }
    return processed;
  }

  /** Run a message through all receive interceptors in registration order. */
  async processReceive(msg: MessageContext): Promise<MessageContext> {
    let processed = { ...msg };
    for (const { interceptor } of this.receiveInterceptors) {
      try {
        await interceptor(processed);
      } catch (err) {
        console.error('[PluginRegistry] Error in receive interceptor:', err);
      }
    }
    return processed;
  }

  // ---------------------------------------------------------------------------
  // Renderers
  // ---------------------------------------------------------------------------

  /** Register a custom renderer contributed by a plugin. */
  registerRenderer(pluginId: string, type: string, renderer: CustomRenderer): void {
    this.renderers.set(type, { pluginId, renderer });
  }

  /** Remove a renderer by type. */
  unregisterRenderer(type: string): void {
    this.renderers.delete(type);
  }

  /** Get the renderer for a given type, if any. */
  getRenderer(type: string): CustomRenderer | undefined {
    return this.renderers.get(type)?.renderer;
  }

  // ---------------------------------------------------------------------------
  // Settings
  // ---------------------------------------------------------------------------

  /** Define the settings schema for a plugin and load persisted values. */
  defineSettings(pluginId: string, schema: SettingsSchema): void {
    this.settingsSchemas.set(pluginId, schema);

    const stored = this.storage.getItem(`plugin:${pluginId}:settings`);
    if (stored) {
      try {
        this.pluginSettings.set(pluginId, JSON.parse(stored));
      } catch (err) {
        console.error(`[PluginRegistry] Failed to load settings for ${pluginId}:`, err);
      }
    } else {
      const defaults: Record<string, unknown> = {};
      for (const [key, def] of Object.entries(schema)) {
        if (def.default !== undefined) defaults[key] = def.default;
      }
      this.pluginSettings.set(pluginId, defaults);
    }
  }

  /** Get a plugin setting value. */
  getPluginSetting<T = unknown>(pluginId: string, key: string): T | undefined {
    return this.pluginSettings.get(pluginId)?.[key] as T | undefined;
  }

  /** Set a plugin setting value and persist it. */
  setPluginSetting(pluginId: string, key: string, value: unknown): void {
    const settings = this.pluginSettings.get(pluginId) ?? {};
    settings[key] = value;
    this.pluginSettings.set(pluginId, settings);
    this.storage.setItem(`plugin:${pluginId}:settings`, JSON.stringify(settings));
  }

  /** Get the settings schema for a plugin. */
  getPluginSettingsSchema(pluginId: string): SettingsSchema | undefined {
    return this.settingsSchemas.get(pluginId);
  }

  // ---------------------------------------------------------------------------
  // Themes
  // ---------------------------------------------------------------------------

  /** Register a theme contributed by a plugin. */
  registerTheme(pluginId: string, theme: PluginTheme): void {
    const fullId = `plugin-${pluginId}-${theme.id}`;
    if (this.themes.has(fullId)) {
      console.warn(`[PluginRegistry] Theme ${fullId} already registered`);
      return;
    }

    const css = generateThemeCSS(fullId, theme);
    this.themes.set(fullId, { pluginId, theme: { ...theme, id: fullId } });
    this.onThemeRegistered?.(fullId, fullId, css);
    console.log(`[PluginRegistry] Theme registered: ${fullId}`);
  }

  /** Unregister a theme contributed by a plugin. */
  unregisterTheme(pluginId: string, themeId: string): void {
    const fullId = `plugin-${pluginId}-${themeId}`;
    if (this.themes.has(fullId)) {
      this.onThemeUnregistered?.(fullId);
      this.themes.delete(fullId);
    }
  }

  /** Return all registered plugin themes. */
  getPluginThemes(): Array<{ id: string; name: string; kind: 'light' | 'dark'; className: string }> {
    return Array.from(this.themes.values()).map(({ theme }) => ({
      id: theme.id,
      name: theme.name,
      kind: theme.kind,
      className: theme.id,
    }));
  }

  // ---------------------------------------------------------------------------
  // Event handlers (generic — wire to Matrix, socket.io, etc.)
  // ---------------------------------------------------------------------------

  /** Track an event handler registered by a plugin. */
  addEventHandler(pluginId: string, eventType: string, handler: (...args: any[]) => void): void {
    if (!this.matrixHandlers.has(eventType)) {
      this.matrixHandlers.set(eventType, []);
    }
    this.matrixHandlers.get(eventType)!.push({ pluginId, handler });
  }

  /** Remove a previously tracked event handler. */
  removeEventHandler(
    pluginId: string,
    eventType: string,
    handler: (...args: any[]) => void
  ): void {
    const handlers = this.matrixHandlers.get(eventType);
    if (handlers) {
      const filtered = handlers.filter(h => h.pluginId !== pluginId || h.handler !== handler);
      this.matrixHandlers.set(eventType, filtered);
    }
  }

  /** Get all tracked handlers for an event type. */
  getEventHandlers(eventType: string): Array<{ pluginId: string; handler: (...args: any[]) => void }> {
    return this.matrixHandlers.get(eventType) ?? [];
  }

  // ---------------------------------------------------------------------------
  // Timers
  // ---------------------------------------------------------------------------

  /** Track a timer ID for a plugin so it can be cleared on unload. */
  addTimer(pluginId: string, timerId: number): void {
    const timers = this.timers.get(pluginId) ?? [];
    timers.push(timerId);
    this.timers.set(pluginId, timers);
  }

  /** Remove a timer ID from tracking (e.g. after manual clearInterval). */
  removeTimer(pluginId: string, timerId: number): void {
    const timers = this.timers.get(pluginId) ?? [];
    this.timers.set(pluginId, timers.filter(id => id !== timerId));
  }

  // ---------------------------------------------------------------------------
  // Logging
  // ---------------------------------------------------------------------------

  /** Append a log entry from a plugin. */
  addLog(pluginId: string, level: 'log' | 'warn' | 'error', args: unknown[]): void {
    this.logs.push({ pluginId, level, timestamp: Date.now(), args });
    if (this.logs.length > this.maxLogs) {
      this.logs = this.logs.slice(-this.maxLogs);
    }
  }

  /** Return log entries, optionally filtered to a specific plugin. */
  getLogs(pluginId?: string): PluginLogEntry[] {
    return pluginId ? this.logs.filter(l => l.pluginId === pluginId) : [...this.logs];
  }

  /** Clear log entries, optionally scoped to a specific plugin. */
  clearLogs(pluginId?: string): void {
    this.logs = pluginId ? this.logs.filter(l => l.pluginId !== pluginId) : [];
  }

  // ---------------------------------------------------------------------------
  // Plugin exports (require())
  // ---------------------------------------------------------------------------

  /** Get the public exports of a loaded plugin. */
  getPluginExports(pluginId: string): unknown {
    return this.plugins.get(pluginId)?.plugin.exports;
  }

  // ---------------------------------------------------------------------------
  // Hot reload
  // ---------------------------------------------------------------------------

  /** Unload the current plugin instance and load a new one in-place. */
  async reloadPlugin(pluginId: string, newPlugin: Plugin, newContext: PluginContext): Promise<void> {
    await this.unregisterPlugin(pluginId);
    this.registerPlugin(pluginId, newPlugin, newContext);
    await newPlugin.onLoad(newContext);
  }

  // ---------------------------------------------------------------------------
  // Teardown
  // ---------------------------------------------------------------------------

  /** Unload all plugins and clear all registry state. */
  clear(): void {
    for (const pluginId of this.plugins.keys()) {
      this.unregisterPlugin(pluginId).catch(err => {
        console.error(`[PluginRegistry] Error clearing plugin ${pluginId}:`, err);
      });
    }
    this.plugins.clear();
    this.commands.clear();
    this.beforeSendInterceptors = [];
    this.receiveInterceptors = [];
    this.renderers.clear();
    this.settingsSchemas.clear();
    this.matrixHandlers.clear();
    this.timers.clear();
    this.logs = [];
  }
}
