import { Injectable, inject } from '@angular/core';
import { environment } from '../../../../../environments/environment';
import { ElectronBridgeService } from '../../../../core/platform/electron/electron-bridge.service';
import type { TojuPluginManifest } from '../../../../shared-kernel';
import {
  DEVELOPMENT_PLUGIN_ENTRYPOINT,
  DEVELOPMENT_PLUGIN_MANIFEST,
  DEVELOPMENT_PLUGIN_MODULE
} from '../../development/development-plugin';
import type {
  TojuClientPluginModule,
  TojuPluginActivationContext,
  TojuPluginDisposable
} from '../../domain/models/plugin-api.models';
import type {
  LocalPluginDiscoveryError,
  LocalPluginRegistrationResult,
  RegisteredPlugin
} from '../../domain/models/plugin-runtime.models';
import { LocalPluginDiscoveryService } from '../../infrastructure/local-plugin-discovery.service';
import { PluginCapabilityService } from './plugin-capability.service';
import { PluginDesktopStateService } from './plugin-desktop-state.service';
import { PluginClientApiService } from './plugin-client-api.service';
import { PluginLoggerService } from './plugin-logger.service';
import { PluginRegistryService } from './plugin-registry.service';
import { PluginUiRegistryService } from './plugin-ui-registry.service';

interface ActivePluginRuntime {
  context: TojuPluginActivationContext;
  moduleObjectUrl?: string;
  module: TojuClientPluginModule;
}

const STORAGE_KEY_PLUGIN_ACTIVATION = 'metoyou_plugin_activation_state';

@Injectable({ providedIn: 'root' })
export class PluginHostService {
  private readonly apiFactory = inject(PluginClientApiService);
  private readonly capabilities = inject(PluginCapabilityService);
  private readonly desktopState = inject(PluginDesktopStateService);
  private readonly electronBridge = inject(ElectronBridgeService, { optional: true });
  private readonly localDiscovery = inject(LocalPluginDiscoveryService);
  private readonly logger = inject(PluginLoggerService);
  private readonly registry = inject(PluginRegistryService);
  private readonly uiRegistry = inject(PluginUiRegistryService);
  private readonly activePlugins = new Map<string, ActivePluginRuntime>();
  private readonly activationRequests = new Map<string, Promise<boolean>>();
  private readonly activationStateReady: Promise<void>;
  private activatedPluginIds = new Set<string>();

  constructor() {
    this.registerDevelopmentPlugin();
    this.activationStateReady = this.loadActivationState();
  }

  registerLocalManifest(manifestValue: unknown, sourcePath?: string): RegisteredPlugin {
    return this.registry.registerManifest(manifestValue, sourcePath);
  }

  async discoverLocalPlugins(): Promise<LocalPluginRegistrationResult> {
    const discovery = await this.localDiscovery.discoverManifests();
    const registered: RegisteredPlugin[] = [];
    const errors: LocalPluginDiscoveryError[] = [...discovery.errors];

    for (const descriptor of discovery.plugins) {
      try {
        registered.push(this.registerLocalManifest(descriptor.manifest, descriptor.pluginRootUrl ?? descriptor.pluginRoot));
      } catch (error) {
        errors.push({
          manifestPath: descriptor.manifestPath,
          message: error instanceof Error ? error.message : 'Plugin manifest validation failed',
          pluginRoot: descriptor.pluginRoot
        });
      }
    }

    return {
      discovery,
      errors,
      registered
    };
  }

  getReadyManifests(): TojuPluginManifest[] {
    return this.registry.loadOrder().ordered;
  }

  async activateReadyPlugins(): Promise<void> {
    await this.activationStateReady;

    const activated: TojuPluginActivationContext[] = [];

    for (const manifest of this.registry.loadOrder().ordered) {
      const entry = this.registry.find(manifest.id);

      if (!entry || !entry.enabled || this.activePlugins.has(manifest.id)) {
        continue;
      }

      const didActivate = await this.activatePlugin(entry);
      const active = this.activePlugins.get(manifest.id);

      if (didActivate && active) {
        activated.push(active.context);
        this.activatedPluginIds.add(active.context.pluginId);
      }
    }

    await this.saveActivationState();

    await this.runReadyHooks(activated);
  }

  async activatePluginById(pluginId: string): Promise<void> {
    await this.activationStateReady;

    if (this.activePlugins.has(pluginId)) {
      this.activatedPluginIds.add(pluginId);
      await this.saveActivationState();
      return;
    }

    const entry = this.registry.find(pluginId);

    if (!entry?.enabled) {
      return;
    }

    const didActivate = await this.activatePlugin(entry);
    const active = this.activePlugins.get(pluginId);

    if (!didActivate || !active) {
      return;
    }

    this.activatedPluginIds.add(pluginId);
    await this.saveActivationState();
    await this.runReadyHooks([active.context]);
  }

  async rememberActivation(pluginId: string): Promise<void> {
    await this.activationStateReady;
    this.activatedPluginIds.add(pluginId);
    await this.saveActivationState();
  }

  async activatePersistedPlugins(): Promise<void> {
    await this.activationStateReady;

    const activated: TojuPluginActivationContext[] = [];

    for (const manifest of this.registry.loadOrder().ordered) {
      if (!this.activatedPluginIds.has(manifest.id) || this.activePlugins.has(manifest.id)) {
        continue;
      }

      const entry = this.registry.find(manifest.id);

      if (!entry?.enabled) {
        continue;
      }

      const didActivate = await this.activatePlugin(entry);
      const active = this.activePlugins.get(manifest.id);

      if (didActivate && active) {
        activated.push(active.context);
      }
    }

    await this.runReadyHooks(activated);
  }

  isPluginActive(pluginId: string): boolean {
    return this.activePlugins.has(pluginId);
  }

  async deactivatePlugin(pluginId: string, options: { forgetActivation?: boolean } = {}): Promise<void> {
    await this.activationStateReady;

    const active = this.activePlugins.get(pluginId);

    if (!active) {
      if (options.forgetActivation) {
        this.activatedPluginIds.delete(pluginId);
        await this.saveActivationState();
      }

      this.registry.setState(pluginId, 'unloaded');
      this.uiRegistry.unregisterPlugin(pluginId);
      return;
    }

    this.registry.setState(pluginId, 'unloading');

    try {
      await active.module.deactivate?.(active.context);
    } catch (error) {
      this.logger.warn(pluginId, 'Plugin deactivate failed', error);
    }

    for (const disposable of [...active.context.subscriptions].reverse()) {
      safeDispose(disposable, pluginId, this.logger);
    }

    this.uiRegistry.unregisterPlugin(pluginId);
    this.activePlugins.delete(pluginId);
    this.revokeModuleObjectUrl(pluginId);

    if (options.forgetActivation) {
      this.activatedPluginIds.delete(pluginId);
      await this.saveActivationState();
    }

    this.registry.setState(pluginId, 'unloaded');
  }

  async deactivateAll(): Promise<void> {
    const pluginIds = Array.from(this.activePlugins.keys()).reverse();

    for (const pluginId of pluginIds) {
      await this.deactivatePlugin(pluginId);
    }
  }

  async reloadPlugin(pluginId: string): Promise<void> {
    await this.deactivatePlugin(pluginId);

    const entry = this.registry.find(pluginId);

    if (entry?.enabled) {
      await this.activatePlugin(entry);

      if (this.activePlugins.has(pluginId)) {
        this.activatedPluginIds.add(pluginId);
        await this.saveActivationState();
      }
    }
  }

  markLoaded(pluginId: string): void {
    this.registry.setState(pluginId, 'loaded');
  }

  markFailed(pluginId: string): void {
    this.registry.setState(pluginId, 'failed');
  }

  private async runReadyHooks(contexts: TojuPluginActivationContext[]): Promise<void> {
    for (const context of contexts) {
      const active = this.activePlugins.get(context.pluginId);

      if (!active?.module.ready) {
        continue;
      }

      try {
        await active.module.ready(context);
        this.registry.setState(context.pluginId, 'ready');
      } catch (error) {
        this.failPlugin(context.pluginId, error);
      }
    }
  }

  private async activatePlugin(entry: RegisteredPlugin): Promise<boolean> {
    const pluginId = entry.manifest.id;

    if (this.activePlugins.has(pluginId)) {
      return false;
    }

    const pendingActivation = this.activationRequests.get(pluginId);

    if (pendingActivation) {
      await pendingActivation;
      return false;
    }

    const activation = this.activatePluginInternal(entry);

    this.activationRequests.set(pluginId, activation);

    try {
      return await activation;
    } finally {
      if (this.activationRequests.get(pluginId) === activation) {
        this.activationRequests.delete(pluginId);
      }
    }
  }

  private async activatePluginInternal(entry: RegisteredPlugin): Promise<boolean> {
    const manifest = entry.manifest;
    const missingCapabilities = this.capabilities.missing(manifest);

    if (missingCapabilities.length > 0) {
      this.registry.setFailed(manifest.id, `Missing capabilities: ${missingCapabilities.join(', ')}`);
      this.logger.warn(manifest.id, 'Plugin blocked by missing capability grants', missingCapabilities);
      return false;
    }

    if (!manifest.entrypoint) {
      this.registry.setState(manifest.id, 'ready');
      return false;
    }

    this.registry.setState(manifest.id, 'loading');

    try {
      const { module, moduleObjectUrl } = await this.loadPluginModule(manifest, entry.sourcePath);
      const context: TojuPluginActivationContext = {
        api: this.apiFactory.createApi(manifest),
        manifest,
        pluginId: manifest.id,
        subscriptions: []
      };

      await this.runWithPluginRuntimeGuards(manifest.id, () => module.activate?.(context));
      this.activePlugins.set(manifest.id, { context, module, moduleObjectUrl });
      this.registry.setState(manifest.id, 'loaded');
      this.logger.info(manifest.id, 'Plugin activated');
      return true;
    } catch (error) {
      this.failPlugin(manifest.id, error);
      return false;
    }
  }

  private failPlugin(pluginId: string, error: unknown): void {
    const message = error instanceof Error ? error.message : 'Plugin activation failed';

    this.registry.setFailed(pluginId, message);
    this.logger.error(pluginId, message, error);
    this.uiRegistry.unregisterPlugin(pluginId);
    this.activePlugins.delete(pluginId);
    this.revokeModuleObjectUrl(pluginId);
  }

  private async runWithPluginRuntimeGuards(pluginId: string, activate: () => Promise<void> | void): Promise<void> {
    const originalMutationObserver = globalThis.MutationObserver;

    if (!originalMutationObserver) {
      await activate();
      return;
    }

    const guardedMutationObserver = createGuardedMutationObserver(originalMutationObserver, pluginId, this.logger);

    globalThis.MutationObserver = guardedMutationObserver;

    try {
      await activate();
    } finally {
      if (globalThis.MutationObserver === guardedMutationObserver) {
        globalThis.MutationObserver = originalMutationObserver;
      }
    }
  }

  private async loadPluginModule(
    manifest: TojuPluginManifest,
    sourcePath?: string
  ): Promise<{ module: TojuClientPluginModule; moduleObjectUrl?: string }> {
    if (manifest.entrypoint === DEVELOPMENT_PLUGIN_ENTRYPOINT) {
      return { module: DEVELOPMENT_PLUGIN_MODULE };
    }

    const entrypointUrl = this.resolveEntrypoint(manifest, sourcePath);

    if (entrypointUrl.startsWith('file://')) {
      const moduleObjectUrl = await this.createLocalModuleObjectUrl(entrypointUrl);
      const module = await import(/* @vite-ignore */ moduleObjectUrl) as TojuClientPluginModule;

      return { module, moduleObjectUrl };
    }

    try {
      return {
        module: await import(/* @vite-ignore */ entrypointUrl) as TojuClientPluginModule
      };
    } catch (error) {
      if (!entrypointUrl.startsWith('http://') && !entrypointUrl.startsWith('https://')) {
        throw error;
      }

      const moduleObjectUrl = await this.createRemoteModuleObjectUrl(entrypointUrl);
      const module = await import(/* @vite-ignore */ moduleObjectUrl) as TojuClientPluginModule;

      return { module, moduleObjectUrl };
    }
  }

  private async createLocalModuleObjectUrl(entrypointUrl: string): Promise<string> {
    const api = this.electronBridge?.getApi();

    if (!api) {
      throw new Error('Local plugin entrypoints require the desktop app');
    }

    const base64Data = await api.readFile(fileUrlToPath(entrypointUrl));
    const bytes = Uint8Array.from(atob(base64Data), (character) => character.charCodeAt(0));
    const source = new TextDecoder().decode(bytes);

    return URL.createObjectURL(new Blob([source], { type: 'text/javascript' }));
  }

  private async createRemoteModuleObjectUrl(entrypointUrl: string): Promise<string> {
    const response = await fetch(entrypointUrl, { headers: { Accept: 'text/javascript,*/*' } });

    if (!response.ok) {
      throw new Error(`Plugin entrypoint returned ${response.status}`);
    }

    const source = await response.text();

    return URL.createObjectURL(new Blob([`${source}\n//# sourceURL=${entrypointUrl}`], { type: 'text/javascript' }));
  }

  private revokeModuleObjectUrl(pluginId: string): void {
    const moduleObjectUrl = this.activePlugins.get(pluginId)?.moduleObjectUrl;

    if (moduleObjectUrl) {
      URL.revokeObjectURL(moduleObjectUrl);
    }
  }

  private registerDevelopmentPlugin(): void {
    if (environment.production) {
      return;
    }

    try {
      this.registry.registerManifest(DEVELOPMENT_PLUGIN_MANIFEST, DEVELOPMENT_PLUGIN_ENTRYPOINT);
    } catch (error) {
      this.logger.warn(DEVELOPMENT_PLUGIN_MANIFEST.id, 'Development plugin registration failed', error);
    }
  }

  private async loadActivationState(): Promise<void> {
    const state = await this.desktopState.readJson<{ activatedPluginIds?: string[] }>(STORAGE_KEY_PLUGIN_ACTIVATION, {});

    this.activatedPluginIds = new Set(
      Array.isArray(state.activatedPluginIds)
        ? state.activatedPluginIds.filter((pluginId): pluginId is string => typeof pluginId === 'string')
        : []
    );
  }

  private async saveActivationState(): Promise<void> {
    await this.desktopState.writeJson(STORAGE_KEY_PLUGIN_ACTIVATION, {
      activatedPluginIds: Array.from(this.activatedPluginIds).sort()
    });
  }

  private resolveEntrypoint(manifest: TojuPluginManifest, sourcePath?: string): string {
    if (!manifest.entrypoint) {
      throw new Error('Plugin entrypoint is missing');
    }

    try {
      return new URL(manifest.entrypoint).toString();
    } catch {}

    if (manifest.bundle?.url && !sourcePath?.startsWith('file://')) {
      return manifest.bundle.url;
    }

    if (sourcePath?.startsWith('http://') || sourcePath?.startsWith('https://') || sourcePath?.startsWith('file://')) {
      return new URL(manifest.entrypoint, sourcePath).toString();
    }

    if (manifest.entrypoint.startsWith('/')) {
      return manifest.entrypoint;
    }

    throw new Error(`Plugin ${manifest.id} has no browser-importable entrypoint`);
  }
}

function fileUrlToPath(fileUrl: string): string {
  const url = new URL(fileUrl);
  const decodedPath = decodeURIComponent(url.pathname);

  if (/^\/[A-Za-z]:\//.test(decodedPath)) {
    return decodedPath.slice(1).replace(/\//g, '\\');
  }

  return decodedPath;
}

function safeDispose(disposable: TojuPluginDisposable, pluginId: string, logger: PluginLoggerService): void {
  try {
    disposable.dispose();
  } catch (error) {
    logger.warn(pluginId, 'Plugin disposable failed', error);
  }
}

function createGuardedMutationObserver(
  NativeMutationObserver: typeof MutationObserver,
  pluginId: string,
  logger: PluginLoggerService
): typeof MutationObserver {
  return class GuardedPluginMutationObserver implements MutationObserver {
    private readonly nativeObserver: MutationObserver;
    private readonly observations: { options?: MutationObserverInit; target: Node }[] = [];
    private isDispatching = false;

    constructor(private readonly callback: MutationCallback) {
      this.nativeObserver = new NativeMutationObserver((records) => this.dispatch(records));
    }

    observe(target: Node, options?: MutationObserverInit): void {
      const existing = this.observations.find((observation) => observation.target === target);

      if (existing) {
        existing.options = options;
      } else {
        this.observations.push({ options, target });
      }

      this.nativeObserver.observe(target, options);
    }

    disconnect(): void {
      this.observations.length = 0;
      this.nativeObserver.disconnect();
    }

    takeRecords(): MutationRecord[] {
      return this.nativeObserver.takeRecords();
    }

    private dispatch(records: MutationRecord[]): void {
      if (this.isDispatching) {
        return;
      }

      this.isDispatching = true;
      this.nativeObserver.disconnect();

      try {
        this.callback(records, this);
      } catch (error) {
        logger.warn(pluginId, 'Plugin MutationObserver callback failed', error);
      } finally {
        this.isDispatching = false;

        for (const observation of this.observations) {
          this.nativeObserver.observe(observation.target, observation.options);
        }
      }
    }
  };
}
