import type { FunctionalComponent } from 'preact';
import { useEffect, useState } from 'preact/hooks';
import type { ModuleId } from '../types/moduleIds';
import { canUseLicenseFeature } from './license/runtime';
import { areRenderEngineCapabilitiesSatisfied, getCurrentRenderEngineKind, getRenderEngineCapabilitiesForKind, isModuleSupportedByEngine, normalizeRenderEngineCapabilityList, type AsukaRenderEngine, type AsukaRenderEngineCapabilityKey, type AsukaRenderEngineKind } from './renderEngine';

/** All supported runtime display modes for a module host. */
export type ModuleDisplayMode =
  | 'floating'
  | 'popover'
  | 'panel-left'
  | 'panel-right'
  | 'panel-bottom'
  | 'panel-top'
  | 'modal'
  | 'inline'
  | 'drawer'
  | 'overlay';

/** Vanilla-friendly UI helper bag injected into custom module renderers. */
export interface AsukaModuleUiHelpers {
  h: typeof import('preact').h;
  el: typeof import('preact').h;
  components: Record<string, unknown>;
  icon: (name: string, options?: { size?: number; className?: string }) => unknown;
}

/** Runtime context passed to custom modules. */
export interface AsukaModuleContext {
  /** Return the currently active rendering engine facade (Fabric or 3D). */
  getEngine: () => AsukaRenderEngine | null;
  /** Vanilla-friendly UI helpers and core components. */
  ui: AsukaModuleUiHelpers;
}

/** Props passed to a mounted custom module component. */
export interface AsukaModuleProps {
  context: AsukaModuleContext;
  ui?: AsukaModuleUiHelpers;
}

/** Component form accepted by the module registry. */
export type AsukaModuleComponent = FunctionalComponent<AsukaModuleProps>;

/** Value returned by lazy module loaders when they mimic an ES module. */
export interface LazyModuleLoaderResult {
  default?: unknown;
}

/** Async loader used for lazy modules. */
export type AsukaModuleLoader = () => Promise<LazyModuleLoaderResult | unknown>;

/** Public custom module definition used by {@link registerModule}. */
export interface AsukaModuleDefinition {
  /** Optional action-only module triggered without opening a UI container. */
  action?: (context: AsukaModuleContext) => void | Promise<void>;
  /** Stable module id used by layout blocks and runtime APIs. */
  id: ModuleId;
  /** Human-readable sidebar/menu label. */
  label?: string;
  /** Optional overlay title override. */
  title?: string;
  /** Icon name or serialized icon token. */
  icon?: string;
  /** Optional grouping used by higher-level UIs. */
  group?: 'dev' | 'prod' | string;
  /** Preferred display mode. */
  displayMode?: ModuleDisplayMode;
  /** Optional premium gate evaluated against the current runtime license. */
  licenseFeature?: string;
  /** Rendering engines supported by this module. Defaults to ['fabric'] when omitted. */
  supportedEngines?: AsukaRenderEngineKind[];
  /** Optional high-level engine capabilities required by this module. */
  requiredCapabilities?: AsukaRenderEngineCapabilityKey[];
  /** Whether this module needs at least one view/canvas to be usable. Defaults to true except for view-independent core modules. */
  requiresView?: boolean;
  /** DOM selector or element id used as a mount target override. */
  mountTarget?: string;
  /** Keep the module mounted when it closes so internal state is preserved. */
  keepMounted?: boolean;
  /** Let pointer and keyboard interaction reach the canvas behind modal or drawer presentations. */
  allowCanvasInteraction?: boolean;
  /** Module render function. */
  render?: AsukaModuleComponent;
  /** Lazy loader used to resolve the module implementation on demand. */
  loader?: AsukaModuleLoader;
  /** Whether the module is currently available. */
  isAvailable?: boolean;
}

/** Callback invoked when the module registry changes. */
export type AsukaModuleRegistryListener = () => void;

/** Global runtime module registry. */
const registry = new Map<ModuleId, AsukaModuleDefinition>();
const pendingDisplayModes = new Map<ModuleId, ModuleDisplayMode>();
const pendingMountTargets = new Map<ModuleId, string>();
const pendingLoads = new Map<ModuleId, Promise<AsukaModuleDefinition | undefined>>();
let registryVersion = 0;
const registryListeners = new Set<AsukaModuleRegistryListener>();

function normalizeModuleDefinition(def: AsukaModuleDefinition): AsukaModuleDefinition {
  return {
    ...def,
    label: (def as any).label ?? (def as any).title ?? String(def.id),
    render: (def as any).render,
    supportedEngines: Array.isArray((def as any).supportedEngines) && (def as any).supportedEngines.length
      ? [...((def as any).supportedEngines as AsukaRenderEngineKind[])]
      : ['fabric'],
    requiredCapabilities: normalizeRenderEngineCapabilityList((def as any).requiredCapabilities),
  } as AsukaModuleDefinition;
}

function emitRegistryChanged(): void {
  registryVersion += 1;
  registryListeners.forEach((listener) => {
    try { listener(); } catch {}
  });
}

/** Subscribe to runtime module registry changes. */
export function subscribeModuleRegistry(listener: AsukaModuleRegistryListener): () => void {
  registryListeners.add(listener);
  return () => registryListeners.delete(listener);
}

/** Force components using the registry hook to re-evaluate dynamic availability gates. */
export function notifyModuleRegistryChanged(): void {
  emitRegistryChanged();
}

/** Return whether a module definition is compatible with the selected engine kind and its capabilities. */
export function isModuleCompatibleWithEngine(
  def: Pick<AsukaModuleDefinition, 'supportedEngines' | 'requiredCapabilities'> | null | undefined,
  engineKind: AsukaRenderEngineKind = getCurrentRenderEngineKind(),
): boolean {
  if (!def) return false;
  if (!isModuleSupportedByEngine(def.supportedEngines, engineKind)) return false;
  const capabilities = getRenderEngineCapabilitiesForKind(engineKind);
  return areRenderEngineCapabilitiesSatisfied(def.requiredCapabilities, capabilities);
}

/** Return normalized capability gates for layout blocks/custom module metadata. */
export function normalizeModuleRequiredCapabilities(value: unknown): AsukaRenderEngineCapabilityKey[] | undefined {
  return normalizeRenderEngineCapabilityList(value);
}

/** React/Preact hook returning a monotonically increasing registry version. */
export function useModuleRegistryVersion(): number {
  const [version, setVersion] = useState(registryVersion);
  useEffect(() => subscribeModuleRegistry(() => setVersion(registryVersion)), []);
  return version;
}

/** Register or replace one module in the global runtime registry. */
export function registerModule(def: AsukaModuleDefinition): void {
  const normalizedDef = normalizeModuleDefinition(def);
  const override = pendingDisplayModes.get(normalizedDef.id);
  const target = pendingMountTargets.get(def.id);
  const finalDef = {
    ...normalizedDef,
    ...(override ? { displayMode: override } : null),
    ...(target ? { mountTarget: target } : null),
  } as AsukaModuleDefinition;
  registry.set(finalDef.id, finalDef);
  emitRegistryChanged();
}

/** Return one module from the registry when it is available. */
export function getModule(id: ModuleId | null | undefined): AsukaModuleDefinition | undefined {
  if (!id) return undefined;
  const def = registry.get(id);
  if (!def || def.isAvailable === false) {
    return undefined;
  }
  if (typeof def.licenseFeature === 'string' && def.licenseFeature.trim().length > 0) {
    if (!canUseLicenseFeature(def.licenseFeature.trim())) {
      return undefined;
    }
  }
  if (!isModuleCompatibleWithEngine(def, getCurrentRenderEngineKind())) {
    return undefined;
  }
  return def;
}

/** Return whether a module exists and is marked as available. */
export function hasModule(id: ModuleId | null | undefined): boolean {
  return !!getModule(id);
}

/** Resolve a lazy module and return its effective runtime definition. */
export async function ensureModuleAvailable(id: ModuleId | null | undefined): Promise<AsukaModuleDefinition | undefined> {
  if (!id) return undefined;
  const current = registry.get(id);
  if (!current || current.isAvailable === false) return undefined;
  if (current.render || current.action) return current;
  if (!current.loader) return current;

  const pending = pendingLoads.get(id);
  if (pending) return pending;

  const task = (async () => {
    try {
      await current.loader?.();
      const next = registry.get(id);
      if (next) {
        const normalized = { ...next, isAvailable: true } as AsukaModuleDefinition;
        registry.set(id, normalized);
        emitRegistryChanged();
        return normalized;
      }
      return undefined;
    } catch {
      registry.set(id, { ...current, isAvailable: false } as AsukaModuleDefinition);
      emitRegistryChanged();
      return undefined;
    } finally {
      pendingLoads.delete(id);
    }
  })();

  pendingLoads.set(id, task);
  return task;
}

/** Return every currently available module definition. */
export function getAllModules(): AsukaModuleDefinition[] {
  const engineKind = getCurrentRenderEngineKind();
  return Array.from(registry.values()).filter((def) => def.isAvailable !== false && isModuleCompatibleWithEngine(def, engineKind));
}

/** Override a module display mode, even before the module is registered. */
export function updateModuleDisplayMode(id: ModuleId, mode: ModuleDisplayMode): void {
  // Runtime layout configuration must survive the lazy implementation replacing
  // its lightweight descriptor after the first open.
  pendingDisplayModes.set(id, mode);
  const def = registry.get(id);
  if (!def || def.displayMode === mode) return;
  registry.set(id, { ...def, displayMode: mode });
  emitRegistryChanged();
}

/**
 * Override the DOM mount target used by ModuleHost.
 *
 * If the module is not registered yet, the target is stored and applied when
 * {@link registerModule} is eventually called.
 */
export function updateModuleMountTarget(id: ModuleId, mountTarget: string | null | undefined): void {
  if (!mountTarget) {
    pendingMountTargets.delete(id);
    const def = registry.get(id);
    if (def && def.mountTarget) {
      const next = { ...def };
      delete (next as any).mountTarget;
      registry.set(id, next);
      emitRegistryChanged();
    }
    return;
  }

  pendingMountTargets.set(id, mountTarget);
  const def = registry.get(id);
  if (!def || def.mountTarget === mountTarget) return;
  registry.set(id, { ...def, mountTarget });
  emitRegistryChanged();
}

/** Return the runtime display mode override applied before/after registration. */
export function getPendingModuleDisplayMode(id: ModuleId): ModuleDisplayMode | null {
  const direct = registry.get(id)?.displayMode;
  if (direct) return direct;
  return pendingDisplayModes.get(id) ?? null;
}

/** Return the runtime mount target override applied before/after registration. */
export function getPendingModuleMountTarget(id: ModuleId): string | null {
  const direct = registry.get(id)?.mountTarget;
  if (direct) return direct;
  return pendingMountTargets.get(id) ?? null;
}
