import { CommonModule } from '@angular/common';
import {
  Component,
  DestroyRef,
  OnInit,
  computed,
  effect,
  inject,
  signal
} from '@angular/core';
import { FormsModule } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { Store as NgRxStore } from '@ngrx/store';
import { NgIcon, provideIcons } from '@ng-icons/core';
import {
  lucideArrowLeft,
  lucideExternalLink,
  lucidePlus,
  lucidePackage,
  lucideRefreshCw,
  lucideSearch,
  lucideSettings,
  lucideStore,
  lucideTrash2,
  lucideX
} from '@ng-icons/lucide';
import { ExternalLinkService } from '../../../../core/platform';
import { SettingsModalService } from '../../../../core/services/settings-modal.service';
import { ChatMessageMarkdownComponent } from '../../../chat';
import { resolveLegacyRole, resolveRoomPermission } from '../../../access-control';
import type {
  PluginCapabilityId,
  Room,
  TojuPluginManifest,
  User
} from '../../../../shared-kernel';
import { selectCurrentRoom, selectSavedRooms } from '../../../../store/rooms/rooms.selectors';
import { selectCurrentUser } from '../../../../store/users/users.selectors';
import { PluginCapabilityService } from '../../application/services/plugin-capability.service';
import { PluginStoreService } from '../../application/services/plugin-store.service';
import type {
  InstalledStorePlugin,
  PluginStoreEntry,
  PluginStoreInstallState,
  PluginStoreReadme
} from '../../domain/models/plugin-store.models';

interface ServerPluginInstallDialog {
  manifest: TojuPluginManifest;
  plugin: PluginStoreEntry;
  selectedServerId: string;
}

@Component({
  selector: 'app-plugin-store',
  standalone: true,
  imports: [
    CommonModule,
    FormsModule,
    ChatMessageMarkdownComponent,
    NgIcon
  ],
  viewProviders: [
    provideIcons({
      lucideArrowLeft,
      lucideExternalLink,
      lucidePlus,
      lucidePackage,
      lucideRefreshCw,
      lucideSearch,
      lucideSettings,
      lucideStore,
      lucideTrash2,
      lucideX
    })
  ],
  templateUrl: './plugin-store.component.html'
})
export class PluginStoreComponent implements OnInit {
  readonly store = inject(PluginStoreService);
  readonly capabilities = inject(PluginCapabilityService);
  readonly ngrxStore = inject(NgRxStore);
  readonly savedRooms = this.ngrxStore.selectSignal(selectSavedRooms);
  readonly currentRoom = this.ngrxStore.selectSignal(selectCurrentRoom);
  readonly currentUser = this.ngrxStore.selectSignal(selectCurrentUser);
  readonly manageableServers = computed(() => {
    const user = this.currentUser();

    if (!user) {
      return [];
    }

    const roomsById = new Map(this.savedRooms().map((room) => [room.id, room]));
    const currentRoom = this.currentRoom();

    if (currentRoom) {
      roomsById.set(currentRoom.id, currentRoom);
    }

    return Array.from(roomsById.values())
      .filter((room) => this.canManageServerPlugins(room, user));
  });
  readonly sourceErrors = computed(() => this.store.sources().filter((source) => !!source.error));
  readonly installedIds = computed(() => new Set(this.store.installedPlugins().map((plugin) => plugin.manifest.id)));
  readonly filteredPlugins = computed(() => {
    const searchTerm = this.searchTerm().trim()
      .toLowerCase();
    const sourceFilter = this.selectedSourceUrl();
    const showInstalled = this.showInstalledOnly();
    const installedIds = this.installedIds();
    const plugins = this.store.availablePlugins()
      .filter((plugin) => !sourceFilter || plugin.sourceUrl === sourceFilter)
      .filter((plugin) => !showInstalled || installedIds.has(plugin.id));

    if (!searchTerm) {
      return plugins;
    }

    return plugins.filter((plugin) => this.matchesSearch(plugin, searchTerm));
  });
  readonly installedCount = computed(() => this.store.installedPlugins().length);
  readonly totalSourcePlugins = computed(() => this.store.availablePlugins().length);
  readonly sourceCount = computed(() => this.store.sourceUrls().length);
  readonly pendingSourceUrls = computed(() => {
    const loadedUrls = new Set(this.store.sources().map((source) => source.url));

    return this.store.sourceUrls().filter((sourceUrl) => !loadedUrls.has(sourceUrl));
  });
  readonly selectedReadmePlugin = computed(() => {
    const readme = this.readme();

    return readme ? this.store.availablePlugins().find((plugin) => plugin.id === readme.pluginId) ?? null : null;
  });
  readonly selectedStoreServer = computed(() => {
    const selectedServerId = this.selectedStoreServerId();

    return selectedServerId ? this.manageableServers().find((server) => server.id === selectedServerId) ?? null : null;
  });

  newSourceUrl = '';
  readonly searchTerm = signal('');
  readonly selectedSourceUrl = signal<string | null>(null);
  readonly selectedStoreServerId = signal<string | null>(null);
  readonly selectedServerInstalledPlugins = signal<InstalledStorePlugin[]>([]);
  readonly showInstalledOnly = signal(false);
  readonly sourceError = signal<string | null>(null);
  readonly actionError = signal<string | null>(null);
  readonly actionBusyPluginId = signal<string | null>(null);
  readonly readme = signal<PluginStoreReadme | null>(null);
  readonly readmeRawMode = signal(false);
  readonly readmeError = signal<string | null>(null);
  readonly readmeLoadingPluginId = signal<string | null>(null);
  readonly serverInstallDialog = signal<ServerPluginInstallDialog | null>(null);
  readonly selectedCapabilityIds = signal<Set<PluginCapabilityId>>(new Set());
  readonly serverInstallOptional = signal(false);
  readonly serverInstallError = signal<string | null>(null);
  readonly serverInstallBusy = signal(false);
  readonly brokenImageKeys = signal<Set<string>>(new Set());

  private destroyed = false;
  private readonly destroyRef = inject(DestroyRef);
  private readonly externalLinks = inject(ExternalLinkService);
  private readonly route = inject(ActivatedRoute);
  private readonly router = inject(Router);
  private readonly settingsModal = inject(SettingsModalService);
  private selectedServerLoadVersion = 0;

  constructor() {
    effect(() => {
      const servers = this.manageableServers();
      const selectedServerId = this.selectedStoreServerId();

      if (servers.length === 0) {
        this.selectedStoreServerId.set(null);
        this.selectedServerInstalledPlugins.set([]);
        return;
      }

      if (!selectedServerId || !servers.some((server) => server.id === selectedServerId)) {
        this.selectedStoreServerId.set(servers[0].id);
        return;
      }

      void this.loadSelectedServerInstalledPlugins(selectedServerId);
    });

    this.destroyRef.onDestroy(() => {
      this.destroyed = true;
    });
  }

  ngOnInit(): void {
    const searchQuery = this.route.snapshot.queryParamMap.get('search')?.trim();

    if (searchQuery) {
      this.searchTerm.set(searchQuery);
    }

    if (this.store.sourceUrls().length > 0 && this.store.sources().length === 0) {
      void this.refreshSources();
    }
  }

  async addSourceUrl(): Promise<void> {
    const sourceUrl = this.newSourceUrl.trim();

    if (!sourceUrl) {
      return;
    }

    this.sourceError.set(null);

    try {
      await this.store.addSourceUrl(sourceUrl);

      if (this.destroyed) {
        return;
      }

      this.newSourceUrl = '';
    } catch (error) {
      if (this.destroyed) {
        return;
      }

      this.sourceError.set(error instanceof Error ? error.message : 'Unable to add plugin source');
    }
  }

  async removeSourceUrl(sourceUrl: string): Promise<void> {
    this.sourceError.set(null);

    try {
      await this.store.removeSourceUrl(sourceUrl);

      if (this.selectedSourceUrl() === sourceUrl) {
        this.selectedSourceUrl.set(null);
      }
    } catch (error) {
      if (this.destroyed) {
        return;
      }

      this.sourceError.set(error instanceof Error ? error.message : 'Unable to remove plugin source');
    }
  }

  async refreshSources(): Promise<void> {
    this.sourceError.set(null);

    try {
      await this.store.refreshSources();
    } catch (error) {
      if (this.destroyed) {
        return;
      }

      this.sourceError.set(error instanceof Error ? error.message : 'Unable to refresh plugin sources');
    }
  }

  async runPrimaryAction(plugin: PluginStoreEntry): Promise<void> {
    const action = this.getPrimaryActionLabel(plugin);

    this.actionError.set(null);
    this.actionBusyPluginId.set(plugin.id);

    try {
      if (action === 'Uninstall') {
        await this.store.uninstallPlugin(plugin.id, plugin.scope);
      } else if (action === 'Remove from Server') {
        await this.store.uninstallPlugin(plugin.id, plugin.scope, { serverId: this.selectedStoreServerId() ?? undefined });
        await this.refreshSelectedServerInstalledPlugins();
      } else if (this.isServerScopedPlugin(plugin)) {
        await this.openServerInstallDialog(plugin);
      } else {
        await this.store.installPlugin(plugin);
      }
    } catch (error) {
      if (this.destroyed) {
        return;
      }

      this.actionError.set(error instanceof Error ? error.message : 'Unable to update plugin installation');
    } finally {
      if (!this.destroyed) {
        this.actionBusyPluginId.set(null);
      }
    }
  }

  async loadReadme(plugin: PluginStoreEntry): Promise<void> {
    this.readmeError.set(null);
    this.readmeLoadingPluginId.set(plugin.id);

    try {
      const readme = await this.store.loadReadme(plugin);

      if (this.destroyed) {
        return;
      }

      this.readme.set(readme);
      this.readmeRawMode.set(false);
    } catch (error) {
      if (this.destroyed) {
        return;
      }

      this.readmeError.set(error instanceof Error ? error.message : 'Unable to load readme');
    } finally {
      if (!this.destroyed) {
        this.readmeLoadingPluginId.set(null);
      }
    }
  }

  closeReadme(): void {
    this.readme.set(null);
    this.readmeRawMode.set(false);
    this.readmeError.set(null);
  }

  toggleReadmeRawMode(): void {
    this.readmeRawMode.update((value) => !value);
  }

  async openServerInstallDialog(plugin: PluginStoreEntry): Promise<void> {
    this.actionBusyPluginId.set(plugin.id);
    this.serverInstallError.set(null);

    try {
      const manifest = await this.store.loadInstallManifest(plugin);
      const selectedServerId = this.selectedStoreServerId();

      if (!selectedServerId) {
        throw new Error('You need owner or Manage Server access on a chat server before installing server plugins');
      }

      this.selectedCapabilityIds.set(new Set(manifest.capabilities ?? []));
      this.serverInstallOptional.set(false);
      this.serverInstallDialog.set({ manifest, plugin, selectedServerId });
    } catch (error) {
      if (this.destroyed) {
        return;
      }

      this.actionError.set(error instanceof Error ? error.message : 'Unable to prepare server plugin install');
    } finally {
      if (!this.destroyed) {
        this.actionBusyPluginId.set(null);
      }
    }
  }

  closeServerInstallDialog(): void {
    if (this.serverInstallBusy()) {
      return;
    }

    this.serverInstallDialog.set(null);
    this.serverInstallError.set(null);
    this.serverInstallOptional.set(false);
    this.selectedCapabilityIds.set(new Set());
  }

  selectServerInstallTarget(serverId: string): void {
    this.selectedStoreServerId.set(serverId);
    this.serverInstallDialog.update((dialog) => dialog ? { ...dialog, selectedServerId: serverId } : dialog);
  }

  selectStoreServer(serverId: string): void {
    this.selectedStoreServerId.set(serverId);
  }

  toggleInstallCapability(capability: PluginCapabilityId, checked: boolean): void {
    this.selectedCapabilityIds.update((capabilities) => {
      const nextCapabilities = new Set(capabilities);

      if (checked) {
        nextCapabilities.add(capability);
      } else {
        nextCapabilities.delete(capability);
      }

      return nextCapabilities;
    });
  }

  async confirmServerInstall(): Promise<void> {
    const dialog = this.serverInstallDialog();

    if (!dialog) {
      return;
    }

    this.serverInstallBusy.set(true);
    this.serverInstallError.set(null);

    try {
      for (const capability of dialog.manifest.capabilities ?? []) {
        if (this.selectedCapabilityIds().has(capability)) {
          this.capabilities.grant(dialog.manifest.id, capability);
        } else {
          this.capabilities.revoke(dialog.manifest.id, capability);
        }
      }

      await this.store.installPlugin(dialog.plugin, {
        activate: true,
        manifest: dialog.manifest,
        optional: this.serverInstallOptional(),
        serverId: dialog.selectedServerId
      });

      await this.loadSelectedServerInstalledPlugins(dialog.selectedServerId);

      if (this.destroyed) {
        return;
      }

      this.serverInstallDialog.set(null);
      this.serverInstallOptional.set(false);
      this.selectedCapabilityIds.set(new Set());
    } catch (error) {
      if (this.destroyed) {
        return;
      }

      this.serverInstallError.set(error instanceof Error ? error.message : 'Unable to install server plugin');
    } finally {
      if (!this.destroyed) {
        this.serverInstallBusy.set(false);
      }
    }
  }

  goBack(): void {
    void this.router.navigateByUrl(this.getReturnUrl());
  }

  async openManager(): Promise<void> {
    const currentRoomId = this.currentRoom()?.id;

    await this.router.navigateByUrl(this.getReturnUrl());
    this.settingsModal.open(this.store.hasActiveServerInstallScope() ? 'serverPlugins' : 'plugins', currentRoomId);
  }

  selectSource(sourceUrl: string | null): void {
    this.selectedSourceUrl.set(sourceUrl);
  }

  toggleInstalledOnly(): void {
    this.showInstalledOnly.update((value) => !value);
  }

  openExternal(url?: string): void {
    if (url) {
      this.externalLinks.open(url);
    }
  }

  isPluginBusy(plugin: PluginStoreEntry): boolean {
    return this.actionBusyPluginId() === plugin.id;
  }

  isReadmeLoading(plugin: PluginStoreEntry): boolean {
    return this.readmeLoadingPluginId() === plugin.id;
  }

  isPrimaryActionDisabled(plugin: PluginStoreEntry): boolean {
    return this.isPluginBusy(plugin)
      || !this.canRunPrimaryAction(plugin)
      || (!plugin.installUrl && this.getPluginInstallState(plugin) !== 'installed');
  }

  canRunPrimaryAction(plugin: PluginStoreEntry): boolean {
    if (!this.isServerScopedPlugin(plugin)) {
      return this.store.canInstallPlugin(plugin);
    }

    return this.manageableServers().length > 0;
  }

  getPluginInstallState(plugin: PluginStoreEntry): PluginStoreInstallState {
    if (!this.isServerScopedPlugin(plugin)) {
      return this.store.getInstallState(plugin);
    }

    const installedPlugin = this.selectedServerInstalledPlugins()
      .find((candidate) => candidate.manifest.id === plugin.id);

    if (!installedPlugin) {
      return 'notInstalled';
    }

    return comparePluginVersions(plugin.version, installedPlugin.manifest.version) > 0
      ? 'updateAvailable'
      : 'installed';
  }

  getPrimaryActionLabel(plugin: PluginStoreEntry): string {
    if (!this.isServerScopedPlugin(plugin)) {
      return this.store.getActionLabel(plugin);
    }

    const state = this.getPluginInstallState(plugin);

    if (state === 'updateAvailable') {
      return 'Update Server';
    }

    return state === 'installed' ? 'Remove from Server' : 'Install to Server';
  }

  primaryActionIcon(plugin: PluginStoreEntry): string {
    const action = this.getPrimaryActionLabel(plugin);

    if (action === 'Uninstall') {
      return 'lucideTrash2';
    }

    if (action === 'Remove from Server') {
      return 'lucideTrash2';
    }

    return 'lucidePlus';
  }

  trackPlugin(index: number, plugin: PluginStoreEntry): string {
    return `${plugin.sourceUrl}:${plugin.id}`;
  }

  hideBrokenImage(event: Event, plugin: PluginStoreEntry): void {
    const image = event.target as HTMLImageElement | null;

    if (image) {
      image.hidden = true;
    }

    const key = this.imageKey(plugin);
    const next = new Set(this.brokenImageKeys());

    next.add(key);
    this.brokenImageKeys.set(next);
  }

  hasBrokenImage(plugin: PluginStoreEntry): boolean {
    return this.brokenImageKeys().has(this.imageKey(plugin));
  }

  private imageKey(plugin: PluginStoreEntry): string {
    return `${plugin.sourceUrl}:${plugin.id}:${plugin.imageUrl ?? ''}`;
  }

  trackServer(index: number, server: Room): string {
    return server.id;
  }

  trackInstallCapability(index: number, capability: PluginCapabilityId): string {
    return capability;
  }

  isServerScopedPlugin(plugin: PluginStoreEntry): boolean {
    return plugin.scope === 'server';
  }

  serverInstallButtonTitle(plugin: PluginStoreEntry): string {
    return this.isServerScopedPlugin(plugin) && this.manageableServers().length === 0
      ? 'Requires owner or Manage Server access on a chat server'
      : this.getPrimaryActionLabel(plugin);
  }

  private matchesSearch(plugin: PluginStoreEntry, searchTerm: string): boolean {
    return [
      plugin.author,
      plugin.description,
      plugin.id,
      plugin.sourceTitle,
      plugin.title,
      plugin.version
    ].some((value) => value?.toLowerCase().includes(searchTerm));
  }

  private getReturnUrl(): string {
    const returnUrl = this.route.snapshot.queryParamMap.get('returnUrl');

    if (returnUrl?.startsWith('/') && !returnUrl.startsWith('//') && !returnUrl.startsWith('/plugin-store')) {
      return returnUrl;
    }

    return '/search';
  }

  private canManageServerPlugins(room: Room, user: User): boolean {
    return resolveLegacyRole(room, user) === 'host' || resolveRoomPermission(room, user, 'manageServer');
  }

  private async refreshSelectedServerInstalledPlugins(): Promise<void> {
    const selectedServerId = this.selectedStoreServerId();

    if (selectedServerId) {
      await this.loadSelectedServerInstalledPlugins(selectedServerId);
    }
  }

  private async loadSelectedServerInstalledPlugins(serverId: string): Promise<void> {
    const loadVersion = ++this.selectedServerLoadVersion;

    try {
      const installedPlugins = await this.store.loadInstalledPluginsForServer(serverId);

      if (!this.destroyed && loadVersion === this.selectedServerLoadVersion && this.selectedStoreServerId() === serverId) {
        this.selectedServerInstalledPlugins.set(installedPlugins);
      }
    } catch {
      if (!this.destroyed && loadVersion === this.selectedServerLoadVersion && this.selectedStoreServerId() === serverId) {
        this.selectedServerInstalledPlugins.set([]);
      }
    }
  }
}

function comparePluginVersions(leftVersion: string, rightVersion: string): number {
  const leftParts = leftVersion.split(/[.-]/).map((part) => Number.parseInt(part, 10) || 0);
  const rightParts = rightVersion.split(/[.-]/).map((part) => Number.parseInt(part, 10) || 0);
  const length = Math.max(leftParts.length, rightParts.length);

  for (let index = 0; index < length; index += 1) {
    const difference = (leftParts[index] ?? 0) - (rightParts[index] ?? 0);

    if (difference !== 0) {
      return difference;
    }
  }

  return 0;
}
