import {
  ChangeDetectionStrategy,
  Component,
  ElementRef,
  inject,
  Input,
  OnDestroy,
  OnInit,
  signal,
  ViewChild,
} from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
import { Subscription } from 'rxjs';
import { trigger, transition, style, animate } from '@angular/animations';
import { PluginService } from '../../plugin.service';
import { PluginBridgeService } from '../../plugin-bridge.service';
import { PluginCleanupService } from '../../plugin-cleanup.service';
import {
  PluginIframeConfig,
  buildPluginIframeHtml,
  handlePluginMessage,
} from '../../util/plugin-iframe.util';
import {
  MatCard,
  MatCardContent,
  MatCardHeader,
  MatCardTitle,
} from '@angular/material/card';
import { TranslateService, TranslatePipe } from '@ngx-translate/core';
import { T } from '../../../t.const';
import { PluginLog } from '../../../core/log';
import { LayoutService } from '../../../core-ui/layout/layout.service';
import { MatButton } from '@angular/material/button';
import { MatIcon } from '@angular/material/icon';
import { MatProgressSpinner } from '@angular/material/progress-spinner';

@Component({
  selector: 'plugin-index',
  templateUrl: './plugin-index.component.html',
  styleUrls: ['./plugin-index.component.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush,
  standalone: true,
  imports: [
    MatButton,
    MatIcon,
    MatProgressSpinner,
    MatCard,
    MatCardContent,
    MatCardHeader,
    MatCardTitle,
    TranslatePipe,
  ],
  animations: [
    trigger('pluginSwitch', [
      transition(':enter', [
        style({ opacity: 0, transform: 'translateX(20px)' }),
        animate('300ms ease-in-out', style({ opacity: 1, transform: 'translateX(0)' })),
      ]),
      transition('* => *', [
        style({ opacity: 0, transform: 'translateX(-20px)' }),
        animate('300ms ease-in-out', style({ opacity: 1, transform: 'translateX(0)' })),
      ]),
    ]),
  ],
})
export class PluginIndexComponent implements OnInit, OnDestroy {
  @ViewChild('iframe', { static: false }) iframeRef?: ElementRef<HTMLIFrameElement>;

  // Optional input for direct plugin ID (for embedding in other components)
  @Input() directPluginId?: string;
  // Optional input to hide the back button and header UI
  @Input() showFullUI: boolean = true;
  // Optional input to use side panel configuration instead of full page
  @Input() useSidePanelConfig: boolean = false;
  // Skip tearing down hook registrations / bridge state on destroy.
  // Set true when embedding the component repeatedly (e.g. work-view embed
  // toggled on/off), so the plugin keeps its registrations across mounts.
  @Input() skipCleanupOnDestroy: boolean = false;

  private readonly _route = inject(ActivatedRoute);
  private readonly _router = inject(Router);
  private readonly _elRef = inject<ElementRef<HTMLElement>>(ElementRef);
  private readonly _sanitizer = inject(DomSanitizer);
  private readonly _pluginService = inject(PluginService);
  private readonly _pluginBridge = inject(PluginBridgeService);
  private readonly _cleanupService = inject(PluginCleanupService);
  private readonly _translateService = inject(TranslateService);
  private readonly _layoutService = inject(LayoutService);

  T = T;

  readonly pluginId = signal<string>('');
  readonly isLoading = signal<boolean>(true);
  readonly error = signal<string | null>(null);
  readonly iframeSrcdoc = signal<SafeHtml | null>(null);
  readonly isResizing = this._layoutService.isPanelResizing;

  private _messageListener?: EventListener;
  private _routeSubscription?: Subscription;

  async ngOnInit(): Promise<void> {
    // If directPluginId is provided, load that plugin directly
    if (this.directPluginId) {
      this.pluginId.set(this.directPluginId);
      // NOTE: no _cleanupIframeCommunication() here. On a fresh mount there is
      // nothing to tear down, and the empty-document srcdoc it would set just
      // forces an extra iframe load + change-detection pass before the real
      // document — widening the load-timing race that blanked the panel (#8394).
      await this._waitForPluginSystem();

      try {
        await this._loadPluginIndex(this.directPluginId);
      } catch (err) {
        PluginLog.err('Failed to load plugin index:', err);
        this.error.set(err instanceof Error ? err.message : 'Failed to load plugin');
        this.isLoading.set(false);
      }
      return;
    }

    // Subscribe to route parameter changes to handle navigation between plugins
    this._routeSubscription = this._route.paramMap.subscribe(async (params) => {
      const newPluginId = params.get('pluginId');
      PluginLog.log(
        'Route paramMap changed, newPluginId:',
        newPluginId,
        'currentPluginId:',
        this.pluginId(),
      );

      if (!newPluginId) {
        this.error.set('Plugin ID not provided');
        this.isLoading.set(false);
        return;
      }

      // Skip if it's the same plugin (prevent unnecessary reloads)
      if (this.pluginId() === newPluginId) {
        PluginLog.log('Same plugin ID, skipping reload');
        return;
      }

      PluginLog.log(
        `Navigating from plugin "${this.pluginId()}" to plugin "${newPluginId}"`,
      );

      // Clean up previous iframe communication BEFORE setting new plugin ID
      this._cleanupIframeCommunication();

      // Reset state when navigating to a different plugin
      this.isLoading.set(true);
      this.error.set(null);
      this.pluginId.set(newPluginId);

      // Wait for plugin system to be initialized
      await this._waitForPluginSystem();

      try {
        await this._loadPluginIndex(newPluginId);
      } catch (err) {
        PluginLog.err('Failed to load plugin index:', err);
        this.error.set(err instanceof Error ? err.message : 'Failed to load plugin');
        this.isLoading.set(false);
      }
    });
  }

  private async _waitForPluginSystem(): Promise<void> {
    // Check if plugin system is already initialized
    if (!this._pluginService.isInitialized()) {
      // Initialize it immediately
      try {
        await this._pluginService.initializePlugins();
      } catch (error) {
        PluginLog.err('Failed to initialize plugin system:', error);
        throw new Error('Failed to initialize plugin system');
      }
    }
  }

  ngOnDestroy(): void {
    this._cleanupIframeCommunication();
    if (this._routeSubscription) {
      this._routeSubscription.unsubscribe();
    }
  }

  goBack(): void {
    this._router.navigate(['/config'], { fragment: 'plugins' });
  }

  private async _loadPluginIndex(pluginId: string): Promise<void> {
    PluginLog.log(`Loading plugin index for: ${pluginId}`);

    // Get the plugin index.html content
    const indexContent = this._pluginService.getPluginIndexHtml(pluginId);
    if (!indexContent) {
      PluginLog.err(`No index.html content found for plugin: ${pluginId}`);
      // Try to get the plugin instance to check if it should have an index.html
      const plugins = await this._pluginService.getAllPlugins();
      const plugin = plugins.find((p) => p.manifest.id === pluginId);

      if (!plugin) {
        throw new Error('Plugin not found');
      }

      if (!plugin.manifest.iFrame) {
        throw new Error('Plugin does not support iframes');
      }

      if (plugin.error) {
        throw new Error(plugin.error);
      }

      throw new Error('Plugin index.html not loaded');
    }

    // Get plugin data for iframe setup
    const plugins = await this._pluginService.getAllPlugins();
    const plugin = plugins.find((p) => p.manifest.id === pluginId);
    if (!plugin) {
      throw new Error('Plugin not found');
    }

    const baseCfg = await this._pluginService.getBaseCfg();

    // Create simple plugin config
    const config: PluginIframeConfig = {
      pluginId,
      manifest: plugin.manifest,
      indexHtml: indexContent,
      baseCfg,
      pluginBridge: this._pluginBridge,
      bridgeToken: this._createBridgeToken(),
      bridgeGeneration: this._pluginService.getPluginIframeGeneration(pluginId),
      boundMethods: this._pluginBridge.createBoundMethods(pluginId, plugin.manifest),
    };

    // Build the iframe document (loaded via srcdoc, not a blob: URL)
    const iframeHtml = buildPluginIframeHtml(config);

    // Store message handler for cleanup
    // Filter by event.source so messages from other plugin iframes (side panel,
    // other embed slots) don't get answered with this plugin's bound methods.
    this._messageListener = async (event: Event) => {
      const msgEvent = event as MessageEvent;
      const iframeWin = this._getPluginIframeWindow();
      if (!iframeWin || msgEvent.source !== iframeWin) {
        return;
      }
      if (
        this._pluginService.getPluginIframeGeneration(config.pluginId) !==
        config.bridgeGeneration
      ) {
        return;
      }
      if (!this._pluginService.getPluginIndexHtml(config.pluginId)) {
        return;
      }
      await handlePluginMessage(msgEvent, config);
    };

    // Set up message communication
    window.addEventListener('message', this._messageListener);

    // Set the iframe document. `srcdoc` is a SecurityContext.HTML sink, so the
    // host-built document (which contains the inline API bridge <script>) must
    // be marked trusted or Angular's HTML sanitizer would strip the script.
    this.iframeSrcdoc.set(this._sanitizer.bypassSecurityTrustHtml(iframeHtml));
    this.isLoading.set(false);
    PluginLog.log(`Plugin ${pluginId} iframe srcdoc set, loading complete`);
  }

  private _createBridgeToken(): string {
    const bytes = new Uint8Array(16);
    crypto.getRandomValues(bytes);
    return Array.from(bytes, (byte) => byte.toString(16).padStart(2, '0')).join('');
  }

  /**
   * Resolve the live window of THIS component's plugin iframe.
   *
   * Reads the rendered DOM rather than the cached `@ViewChild`: with zoneless
   * change detection the ViewChild property is only assigned on the next CD
   * pass (rAF/timeout), which can land AFTER the srcdoc iframe has executed its
   * scripts and posted its first (tokenless) PLUGIN_MESSAGE. Because a message
   * can only exist once its iframe is in the DOM, querying this component's
   * host subtree at message time always finds our iframe — so we no longer
   * silently drop the plugin's own messages and leave the panel blank (#8394).
   *
   * Still scoped to this component's host, so messages from sibling plugin
   * iframes (side panel vs. embed slot) are not answered with our bound methods.
   */
  private _getPluginIframeWindow(): Window | null {
    const iframeEl = this._elRef.nativeElement.querySelector<HTMLIFrameElement>(
      'iframe[data-plugin-iframe]',
    );
    return iframeEl?.contentWindow ?? null;
  }

  private _cleanupIframeCommunication(): void {
    const currentPluginId = this.pluginId();
    PluginLog.log(`Cleaning up iframe communication for plugin: ${currentPluginId}`);

    // Remove message listener
    if (this._messageListener) {
      window.removeEventListener('message', this._messageListener);
      this._messageListener = undefined;
      PluginLog.log(`Removed message listener for plugin: ${currentPluginId}`);
    }

    // Clear iframe reference from cleanup service (but don't remove from DOM).
    // Skip when embedding (work-view, side panel) so the plugin keeps its hook
    // and button registrations across embed mount/unmount cycles.
    if (currentPluginId && !this.skipCleanupOnDestroy) {
      this._cleanupService.cleanupPlugin(currentPluginId);
      PluginLog.log(`Cleaned up plugin references for: ${currentPluginId}`);
    }

    // Reset iframe to an empty document to stop execution but keep it in DOM
    this.iframeSrcdoc.set(
      this._sanitizer.bypassSecurityTrustHtml('<html><body></body></html>'),
    );
    PluginLog.log(`Reset iframe to empty document for plugin: ${currentPluginId}`);
  }

  onIframeLoad(): void {
    PluginLog.log('Plugin iframe loaded for plugin:', this.pluginId());

    // Reading the cached @ViewChild is fine HERE (unlike the message listener,
    // which uses _getPluginIframeWindow()): this fires on the iframe's `load`
    // event, by which point the ViewChild is assigned, and registration is not
    // race-sensitive. Do not "unify" this with the listener's DOM lookup.
    if (this.iframeRef?.nativeElement && this.pluginId()) {
      this._cleanupService.registerIframe(this.pluginId(), this.iframeRef.nativeElement);
    }
  }
}
