import type {
  EventsAPI,
  Plugin,
  PluginAPI,
  PluginSettings,
  SEQTAAPI,
  SettingsAPI,
  SettingValue,
  StorageAPI,
} from "./types";
import { eventManager } from "@/seqta/utils/listeners/EventManager";
import ReactFiber from "@/seqta/utils/ReactFiber";
import browser from "webextension-polyfill";
import { settingsState } from "@/seqta/utils/listeners/SettingsState";
import type { SettingsState } from "@/types/storage";

function createSEQTAAPI(): SEQTAAPI {
  return {
    onMount: (selector, callback) => {
      return eventManager.register(
        `${selector}Added`,
        {
          selector,
          customCheck: (element) => element.matches(selector),
        },
        callback,
      );
    },
    getFiber: (selector) => {
      return ReactFiber.find(selector);
    },
    getCurrentPage: () => {
      const path = window.location.hash.split("?page=/")[1] || "";
      return path.split("/")[0];
    },
    onPageChange: (callback) => {
      const handler = () => {
        const page = window.location.hash.split("?page=/")[1] || "";
        callback(page.split("/")[0]);
      };

      window.addEventListener("hashchange", handler);

      // Return an unregister function
      return {
        unregister: () => {
          window.removeEventListener("hashchange", handler);
        },
      };
    },
  };
}

/**
 * Creates a reactive and persistent settings store for a given plugin.
 * This store is a Svelte-like store, providing reactivity, persistence
 * via `browser.storage.local`, and default value handling.
 *
 * @template T - Represents the structure of the plugin's settings, extending `PluginSettings`.
 * @param {Plugin<T, any>} plugin The plugin instance for which the settings store is being created.
 *                                `plugin.id` is used for namespacing the settings in storage,
 *                                and `plugin.settings` provides the definitions and default values for each setting.
 * @returns {SettingsAPI<T> & { loaded: Promise<void> }} An object that functions as a Svelte store,
 *                                                       enhanced with specific methods for settings management.
 *                                                       The object includes:
 *    - Reactivity: Changes to settings can be subscribed to using Svelte's store subscription pattern
 *                  (though not explicitly a Svelte store, it behaves similarly for direct property access and updates).
 *                  The `onChange` method provides a more direct way to listen for specific key changes.
 *    - Persistence: Settings are automatically loaded from `browser.storage.local` when the store is created
 *                   and saved back whenever a setting is changed via the proxy's setter.
 *    - Default Values: Uses default values from the `plugin.settings` definition if no stored value exists for a setting.
 *    - `loaded`: A Promise that resolves when the settings have been successfully loaded from storage,
 *                allowing operations to be deferred until settings are ready.
 *    - Direct property access for getting values (e.g., `settingsStore.mySettingKey`).
 *    - Direct property assignment for setting values (e.g., `settingsStore.mySettingKey = newValue`), which also persists the change.
 *    - `onChange(key, callback)`: Method to listen for changes to a specific setting. (Note: The prompt mentioned `listen`, this is `onChange`).
 *                                 Returns an object with an `unregister` method.
 *    - `offChange(key, callback)`: Method to stop listening for changes to a specific setting.
 *    The following methods are not explicitly present on the returned proxy from `createSettingsAPI` but are typically
 *    expected in a full "Svelte store" settings manager. The current implementation relies on direct property
 *    manipulation for get/set, and re-initialization for reset-like behavior or would require external implementation
 *    of reset logic if needed:
 *    - `get(key)`: (Achieved by direct property access: `settingsStore.key`)
 *    - `set(key, value)`: (Achieved by direct property assignment: `settingsStore.key = value`)
 *    - `reset(key)`: (Would require manual re-application of `plugin.settings[key].default` and then setting it)
 *    - `resetAll()`: (Would require iterating through all `plugin.settings` and applying defaults, then setting them)
 */
function createSettingsAPI<T extends PluginSettings>(
  plugin: Plugin<T>,
): {
  settings: SettingsAPI<T> & { loaded: Promise<void> };
  dispose: () => void;
} {
  const storageKey = `plugin.${plugin.id}.settings`;
  const listeners = new Map<keyof T, Set<(value: any) => void>>();

  // Initialize with default values
  const settingsWithMeta: any = {
    onChange: <K extends keyof T>(
      key: K,
      callback: (value: SettingValue<T[K]>) => void,
    ) => {
      if (!listeners.has(key)) {
        listeners.set(key, new Set());
      }
      listeners.get(key)!.add(callback);
      return {
        unregister: () => {
          listeners.get(key)!.delete(callback);
        },
      };
    },
    offChange: <K extends keyof T>(
      key: K,
      callback: (value: SettingValue<T[K]>) => void,
    ) => {
      listeners.get(key)?.delete(callback);
    },
    loaded: Promise.resolve(), // will be replaced below
  };

  // Fill with defaults first
  for (const key in plugin.settings) {
    if (plugin.settings[key].type !== 'component' && plugin.settings[key].type !== 'button') {
      settingsWithMeta[key] = plugin.settings[key].default;
    }
  }

  // Load stored settings and override defaults
  const loaded = (async () => {
    try {
      const allSettings = settingsState.getAll() as unknown as Record<string, unknown>;
      const storedSettings = allSettings[storageKey] as Partial<Record<keyof T, any>>;
      if (storedSettings) {
        for (const key in storedSettings) {
          if (key in settingsWithMeta) {
            settingsWithMeta[key] = storedSettings[key];
            listeners
              .get(key as keyof T)
              ?.forEach((cb) => cb(storedSettings[key]));
          }
        }
      }
    } catch (error) {
      console.error(
        `[BetterSEQTA+] Error loading settings for plugin ${plugin.id}:`,
        error,
      );
    }
  })();

  settingsWithMeta.loaded = loaded;

  const handleSettingsChange = (newValue: unknown) => {
    if (!newValue || typeof newValue !== "object") return;

    const newSettings = newValue as Partial<Record<keyof T, any>>;
    for (const key in newSettings) {
      const typedKey = key as keyof T;
      settingsWithMeta[typedKey] = newSettings[typedKey];
      listeners.get(typedKey)?.forEach((cb) => cb(newSettings[typedKey]));
    }
  };

  settingsState.register(
    storageKey as keyof SettingsState,
    handleSettingsChange,
  );

  const dispose = () => {
    settingsState.unregister(
      storageKey as keyof SettingsState,
      handleSettingsChange,
    );
  };

  const proxy = new Proxy(settingsWithMeta, {
    get(target, prop) {
      return target[prop];
    },
    set(target, prop, value) {
      if (["onChange", "offChange", "loaded"].includes(prop as string))
        return false;

      target[prop] = value;

      // Reconstruct just the data keys for storage (excluding metadata methods)
      const dataToStore: any = {};
      for (const key in plugin.settings) {
        dataToStore[key] = target[key];
      }

      // Preserve enabled flag managed separately for disableToggle plugins
      if (plugin.disableToggle) {
        const allSettings = settingsState.getAll() as Record<string, unknown>;
        const existing = allSettings[storageKey] as
          | { enabled?: boolean }
          | undefined;
        if (existing?.enabled !== undefined) {
          dataToStore.enabled = existing.enabled;
        }
      }

      browser.storage.local.set({ [storageKey]: dataToStore });

      listeners.get(prop as keyof T)?.forEach((cb) => cb(value));
      return true;
    },
  }) as SettingsAPI<T> & { loaded: Promise<void> };

  return { settings: proxy, dispose };
}

function createStorageAPI<T = any>(
  pluginId: string,
): {
  storage: StorageAPI<T> & { [K in keyof T]: T[K] };
  dispose: () => void;
} {
  const prefix = `plugin.${pluginId}.storage.`;
  const cache: Record<string, any> = {};
  const listeners = new Map<string, Set<(value: any) => void>>();

  // Load all existing storage values for this plugin
  const loadStoragePromise = (async () => {
    try {
      const allStorage = settingsState.getAll();

      // Filter for this plugin's storage keys and populate cache
      Object.entries(allStorage).forEach(([key, value]) => {
        if (key.startsWith(prefix)) {
          const shortKey = key.slice(prefix.length);
          cache[shortKey] = value;
        }
      });
    } catch (error) {
      console.error(
        `[BetterSEQTA+] Error loading storage for plugin ${pluginId}:`,
        error,
      );
    }
  })();

  const handleStorageChange = (
    newValue: unknown,
    _oldValue: unknown,
    key: string,
  ) => {
    if (!key.startsWith(prefix)) return;

    const shortKey = key.slice(prefix.length);
    cache[shortKey] = newValue;
    listeners.get(shortKey)?.forEach((callback) => callback(newValue));
  };

  settingsState.registerGlobal(handleStorageChange);

  const dispose = () => {
    settingsState.unregisterGlobal(handleStorageChange);
  };

  // Create the proxy for direct property access
  const storage = new Proxy(cache, {
    get(target, prop: string) {
      if (prop === "onChange") {
        return (key: keyof T, callback: (value: T[keyof T]) => void) => {
          if (!listeners.has(key as string)) {
            listeners.set(key as string, new Set());
          }
          listeners.get(key as string)!.add(callback);
          return {
            unregister: () => {
              listeners.get(key as string)?.delete(callback);
            },
          };
        };
      }
      if (prop === "offChange") {
        return (key: keyof T, callback: (value: T[keyof T]) => void) => {
          listeners.get(key as string)?.delete(callback);
        };
      }
      if (prop === "loaded") {
        return loadStoragePromise;
      }

      // Direct property access
      return target[prop];
    },
    set(target, prop: string, value: any) {
      if (["onChange", "offChange", "loaded"].includes(prop)) {
        return false;
      }

      // Update cache and store in browser storage
      target[prop] = value;
      browser.storage.local.set({ [prefix + prop]: value });

      // Notify listeners
      listeners.get(prop)?.forEach((callback) => callback(value));

      return true;
    },
  }) as StorageAPI<T> & { [K in keyof T]: T[K] };

  return { storage, dispose };
}

function createEventsAPI(pluginId: string): EventsAPI {
  const prefix = `plugin.${pluginId}.`;
  const eventListeners = new Map<
    string,
    Set<{ callback: (...args: any[]) => void; listener: EventListener }>
  >();

  return {
    on: (event, callback) => {
      const fullEventName = prefix + event;
      const listener = ((e: CustomEvent) => {
        callback(...(e.detail || []));
      }) as EventListener;

      document.addEventListener(fullEventName, listener);

      if (!eventListeners.has(event)) {
        eventListeners.set(event, new Set());
      }
      eventListeners.get(event)!.add({ callback, listener });

      return {
        unregister: () => {
          document.removeEventListener(fullEventName, listener);
          eventListeners.get(event)?.delete({ callback, listener });
        },
      };
    },
    emit: (event, ...args) => {
      document.dispatchEvent(
        new CustomEvent(prefix + event, {
          detail: args.length > 0 ? args : null,
        }),
      );
    },
  };
}

/**
 * Creates and returns a tailored API object for a specific plugin.
 * This API object provides the plugin with various functionalities such as
 * managing settings, accessing namespaced storage, interacting with SEQTA-specific features,
 * and handling plugin-specific events.
 *
 * @template T - The type of the plugin's settings, extending `PluginSettings`.
 * @template S - The type of the data the plugin will store in its namespaced storage.
 * @param {Plugin<T, S>} plugin The plugin instance for which the API is being created.
 *                              The plugin's `id` and `name` are used internally by the API
 *                              for namespacing and identification but are accessed from the `plugin` object directly.
 * @returns {PluginAPI<T, S>} An API object containing the following key properties:
 *    - `seqta`: An API for interacting with SEQTA-specific functionalities, created by `createSEQTAAPI()`.
 *               This includes methods like `onMount` for DOM element appearance, `getFiber` for React component inspection,
 *               `getCurrentPage` for getting the current SEQTA page, and `onPageChange` for listening to page navigations.
 *    - `settings`: An API for managing plugin-specific settings, created by `createSettingsAPI(plugin)`.
 *                  It allows getting, setting, and listening to changes in the plugin's settings,
 *                  which are stored persistently and namespaced to the plugin. Includes a `loaded` promise.
 *    - `storage`: An API for providing namespaced storage for the plugin, created by `createStorageAPI<S>(plugin.id)`.
 *                 It allows the plugin to store and retrieve arbitrary data, namespaced to prevent conflicts
 *                 with other plugins or parts of the extension. Includes a `loaded` promise and `onChange` listeners.
 *    - `events`: An API for allowing the plugin to dispatch and listen for custom events within its own scope,
 *                created by `createEventsAPI(plugin.id)`. It provides `on(event, callback)` to listen for
 *                plugin-specific events and `emit(event, ...args)` to dispatch them. These events are namespaced
 *                to the plugin.
 */
export function createPluginAPI<T extends PluginSettings, S = any>(
  plugin: Plugin<T, S>,
): PluginAPI<T, S> {
  const { settings, dispose: disposeSettings } = createSettingsAPI(plugin);
  const { storage, dispose: disposeStorage } = createStorageAPI<S>(plugin.id);

  return {
    seqta: createSEQTAAPI(),
    settings,
    storage,
    events: createEventsAPI(plugin.id),
    dispose: () => {
      disposeSettings();
      disposeStorage();
    },
  };
}
