/* eslint-disable @typescript-eslint/member-ordering */
import {
  Component,
  effect,
  inject,
  Injector,
  Input,
  OnInit,
  signal
} from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { Router } from '@angular/router';
import { Store } from '@ngrx/store';
import {
  debounceTime,
  distinctUntilChanged,
  firstValueFrom,
  Subject
} from 'rxjs';
import { NgIcon, provideIcons } from '@ng-icons/core';
import {
  lucideExternalLink,
  lucideFileText,
  lucideSearch,
  lucideUsers,
  lucideLock,
  lucideGlobe,
  lucideChevronDown
} from '@ng-icons/lucide';

import { AppI18nService, APP_TRANSLATE_IMPORTS } from '../../../../core/i18n';
import { setStoredCurrentUserId } from '../../../../core/storage/current-user-storage';
import { buildLoginReturnQueryParams } from '../../../authentication/domain/logic/auth-navigation.rules';
import { SignalServerAuthorizeService } from '../../../authentication/application/services/signal-server-authorize.service';
import { AutoFocusDirective, SelectOnFocusDirective } from '../../../../shared/directives';
import { RoomsActions } from '../../../../store/rooms/rooms.actions';
import {
  selectSearchResults,
  selectIsSearching,
  selectRoomsError,
  selectSavedRooms
} from '../../../../store/rooms/rooms.selectors';
import {
  Room,
  User,
  type PluginRequirementSummary
} from '../../../../shared-kernel';
import { ExternalLinkService } from '../../../../core/platform';
import { DatabaseService } from '../../../../infrastructure/persistence';
import { type ServerInfo } from '../../domain/models/server-directory.model';
import { ServerDirectoryFacade } from '../../application/facades/server-directory.facade';
import { selectCurrentUser } from '../../../../store/users/users.selectors';
import {
  ConfirmDialogComponent,
  LeaveServerDialogComponent,
  ModalBackdropComponent,
  type LeaveServerDialogResult
} from '../../../../shared';
import { ChatMessageMarkdownComponent } from '../../../chat';
import { hasRoomBanForUser } from '../../../access-control';
import { RealtimeSessionFacade } from '../../../../core/realtime';
import {
  PluginRequirementService,
  PluginStoreService,
  type PluginStoreReadme
} from '../../../plugins';

interface JoinPluginConsentDialog {
  optional: PluginRequirementSummary[];
  password?: string;
  required: PluginRequirementSummary[];
  server: ServerInfo;
}

/** A named group of servers rendered when the browser is not in active search mode. */
export interface ServerDiscoverySection {
  id: string;
  title: string;
  subtitle?: string;
  servers: ServerInfo[];
}

/**
 * Reusable server discovery + join surface. Owns the full join flow (password prompt,
 * plugin-consent, banned, plugin readme) and the leave-server dialog, and renders both
 * live search results and any caller-supplied discovery sections with the same card UI.
 */
@Component({
  selector: 'app-server-browser',
  standalone: true,
  imports: [
    CommonModule,
    FormsModule,
    NgIcon,
    ChatMessageMarkdownComponent,
    ConfirmDialogComponent,
    LeaveServerDialogComponent,
    ModalBackdropComponent,
    AutoFocusDirective,
    SelectOnFocusDirective,
    ...APP_TRANSLATE_IMPORTS
  ],
  viewProviders: [
    provideIcons({
      lucideExternalLink,
      lucideFileText,
      lucideSearch,
      lucideUsers,
      lucideLock,
      lucideGlobe,
      lucideChevronDown
    })
  ],
  templateUrl: './server-browser.component.html'
})
export class ServerBrowserComponent implements OnInit {
  private store = inject(Store);
  private router = inject(Router);
  private db = inject(DatabaseService);
  private externalLinks = inject(ExternalLinkService);
  private serverDirectory = inject(ServerDirectoryFacade);
  private webrtc = inject(RealtimeSessionFacade);
  private pluginRequirements = inject(PluginRequirementService);
  private pluginStore = inject(PluginStoreService);
  private injector = inject(Injector);
  private readonly i18n = inject(AppI18nService);
  private readonly signalServerAuthorize = inject(SignalServerAuthorizeService);
  private searchSubject = new Subject<string>();
  private banLookupRequestVersion = 0;

  /** Discovery sections shown when the search query is empty. */
  @Input() discoverySections: ServerDiscoverySection[] = [];
  /** Title for the onboarding empty state when there is nothing to show. */
  @Input() emptyStateTitle?: string;
  /** Supporting copy for the onboarding empty state. */
  @Input() emptyStateMessage?: string;
  /** Placeholder for the search input. */
  @Input() searchPlaceholder?: string;
  /** Whether the My Servers quick bar is shown. */
  @Input() showMyServers = true;

  get resolvedEmptyStateTitle(): string {
    return this.emptyStateTitle ?? this.i18n.instant('servers.browser.empty.title');
  }

  get resolvedEmptyStateMessage(): string {
    return this.emptyStateMessage ?? this.i18n.instant('servers.browser.empty.message');
  }

  get resolvedSearchPlaceholder(): string {
    return this.searchPlaceholder ?? this.i18n.instant('servers.browser.search.placeholder');
  }

  serverCardTitle(server: ServerInfo): string {
    return this.isJoinedServer(server)
      ? this.i18n.instant('servers.browser.card.doubleClickOpen', { name: server.name })
      : this.i18n.instant('servers.browser.card.doubleClickJoin', { name: server.name });
  }

  serverActionsLabel(server: ServerInfo): string {
    return this.i18n.instant('servers.browser.card.serverActions', { name: server.name });
  }

  joinServerLabel(server: ServerInfo): string {
    return this.i18n.instant('servers.browser.card.joinServer', { name: server.name });
  }

  ownerLabel(server: ServerInfo): string {
    return this.i18n.instant('servers.browser.card.owner', { name: this.getServerOwnerLabel(server) });
  }

  bannedDialogMessage(): string {
    return this.i18n.instant('servers.browser.bannedDialog.message', {
      name: this.bannedServerName() || this.i18n.instant('servers.browser.bannedDialog.thisServer')
    });
  }

  passwordDialogMessage(server: ServerInfo): string {
    return this.i18n.instant('servers.browser.passwordDialog.message', { name: server.name });
  }

  pluginUsesPluginsLabel(serverName: string): string {
    return this.i18n.instant('servers.plugins.usesPlugins', { name: serverName });
  }

  pluginConsentConfirmLabel(requiredCount: number): string {
    if (this.pluginConsentBusy()) {
      return this.i18n.instant('servers.plugins.downloading');
    }

    return requiredCount > 0
      ? this.i18n.instant('servers.plugins.acceptAndJoin')
      : this.i18n.instant('servers.plugins.join');
  }

  pluginReadmeButtonLabel(pluginId: string): string {
    return this.pluginConsentReadmeLoadingId() === pluginId
      ? this.i18n.instant('common.labels.loading')
      : this.i18n.instant('servers.plugins.readme');
  }

  searchQuery = '';
  searchResults = this.store.selectSignal(selectSearchResults);
  isSearching = this.store.selectSignal(selectIsSearching);
  error = this.store.selectSignal(selectRoomsError);
  savedRooms = this.store.selectSignal(selectSavedRooms);
  currentUser = this.store.selectSignal(selectCurrentUser);
  activeEndpoints = this.serverDirectory.activeServers;
  bannedServerLookup = signal<Record<string, boolean>>({});
  bannedServerName = signal('');
  showBannedDialog = signal(false);
  showPasswordDialog = signal(false);
  passwordPromptServer = signal<ServerInfo | null>(null);
  joinPassword = signal('');
  joinPasswordError = signal<string | null>(null);
  joinErrorMessage = signal<string | null>(null);
  joinedServerMenuId = signal<string | null>(null);
  leaveDialogRoom = signal<Room | null>(null);
  pluginConsentDialog = signal<JoinPluginConsentDialog | null>(null);
  selectedOptionalPluginIds = signal<Set<string>>(new Set());
  pluginConsentBusy = signal(false);
  pluginConsentError = signal<string | null>(null);
  pluginConsentReadme = signal<PluginStoreReadme | null>(null);
  pluginConsentReadmeLoadingId = signal<string | null>(null);
  pluginConsentReadmeError = signal<string | null>(null);

  // The reactive effect is created in ngOnInit with an explicit injector so the
  // component can be instantiated outside a change-detection context (e.g. unit tests).
  ngOnInit(): void {
    effect(
      () => {
        const servers = this.searchResults();
        const currentUser = this.currentUser();

        void this.refreshBannedLookup(servers, currentUser ?? null);
        void this.requestMissingServerIcons(servers, currentUser ?? null);
      },
      { injector: this.injector }
    );

    this.store.dispatch(RoomsActions.searchServers({ query: '' }));
    this.store.dispatch(RoomsActions.loadRooms());

    this.searchSubject.pipe(debounceTime(120), distinctUntilChanged()).subscribe((query) => {
      this.store.dispatch(RoomsActions.searchServers({ query }));
    });
  }

  /** True while the user is actively searching (non-empty query). */
  get isSearchMode(): boolean {
    return this.searchQuery.trim().length > 0;
  }

  /** Discovery sections that actually contain servers. */
  get visibleSections(): ServerDiscoverySection[] {
    return this.discoverySections.filter((section) => section.servers.length > 0);
  }

  /** True when there is nothing to render outside of search mode. */
  get showEmptyState(): boolean {
    return !this.isSearchMode && this.visibleSections.length === 0;
  }

  onSearchChange(query: string): void {
    this.searchSubject.next(query);
  }

  async joinServer(server: ServerInfo): Promise<void> {
    const currentUser = this.currentUser();
    const currentUserId = localStorage.getItem('metoyou_currentUserId') || currentUser?.id;

    if (!currentUserId) {
      this.router.navigate(['/login'], {
        queryParams: buildLoginReturnQueryParams(this.router.url)
      });

      return;
    }

    setStoredCurrentUserId(currentUserId);

    if (await this.isServerBanned(server)) {
      this.bannedServerName.set(server.name);
      this.showBannedDialog.set(true);
      return;
    }

    await this.attemptJoinServer(server);
  }

  joinSavedRoom(room: Room): void {
    this.openJoinedRoom(room);
  }

  openServerCard(server: ServerInfo): void {
    const joinedRoom = this.joinedRoomForServer(server);

    if (joinedRoom) {
      this.openJoinedRoom(joinedRoom);
      return;
    }

    void this.joinServer(server);
  }

  joinedRoomForServer(server: ServerInfo): Room | null {
    return this.savedRooms().find((room) => room.id === server.id) ?? null;
  }

  isJoinedServer(server: ServerInfo): boolean {
    return !!this.joinedRoomForServer(server);
  }

  toggleJoinedServerMenu(event: Event, server: ServerInfo): void {
    event.stopPropagation();
    this.joinedServerMenuId.update((currentId) => (currentId === server.id ? null : server.id));
  }

  closeJoinedServerMenu(): void {
    this.joinedServerMenuId.set(null);
  }

  openLeaveDialog(event: Event, server: ServerInfo): void {
    event.stopPropagation();
    const room = this.joinedRoomForServer(server);

    if (!room) {
      return;
    }

    this.joinedServerMenuId.set(null);
    this.leaveDialogRoom.set(room);
  }

  closeLeaveDialog(): void {
    this.leaveDialogRoom.set(null);
  }

  confirmLeaveServer(result: LeaveServerDialogResult): void {
    const room = this.leaveDialogRoom();

    if (!room) {
      return;
    }

    this.store.dispatch(
      RoomsActions.forgetRoom({
        roomId: room.id,
        nextOwnerKey: result.nextOwnerKey
      })
    );

    this.leaveDialogRoom.set(null);
  }

  closeBannedDialog(): void {
    this.showBannedDialog.set(false);
    this.bannedServerName.set('');
  }

  closePasswordDialog(): void {
    this.showPasswordDialog.set(false);
    this.passwordPromptServer.set(null);
    this.joinPassword.set('');
    this.joinPasswordError.set(null);
  }

  closePluginConsentDialog(): void {
    if (this.pluginConsentBusy()) {
      return;
    }

    this.pluginConsentDialog.set(null);
    this.selectedOptionalPluginIds.set(new Set());
    this.pluginConsentError.set(null);
    this.closePluginConsentReadme();
  }

  toggleOptionalPluginInstall(pluginId: string, checked: boolean): void {
    this.selectedOptionalPluginIds.update((selectedIds) => {
      const nextIds = new Set(selectedIds);

      if (checked) {
        nextIds.add(pluginId);
      } else {
        nextIds.delete(pluginId);
      }

      return nextIds;
    });
  }

  async confirmPluginConsent(): Promise<void> {
    const dialog = this.pluginConsentDialog();

    if (!dialog) {
      return;
    }

    const selectedOptionalIds = this.selectedOptionalPluginIds();
    const acceptedRequirements = dialog.required.concat(
      dialog.optional.filter((requirement) => selectedOptionalIds.has(requirement.pluginId))
    );

    this.pluginConsentBusy.set(true);
    this.pluginConsentError.set(null);

    try {
      await this.attemptJoinServer(dialog.server, dialog.password, {
        acceptedRequirements,
        skipPluginConsent: true
      });

      this.pluginConsentDialog.set(null);
      this.selectedOptionalPluginIds.set(new Set());
      this.closePluginConsentReadme();
    } catch (error) {
      this.pluginConsentError.set(error instanceof Error ? error.message : this.i18n.instant('servers.errors.installPluginsFailed'));
    } finally {
      this.pluginConsentBusy.set(false);
    }
  }

  async openPluginConsentReadme(requirement: PluginRequirementSummary): Promise<void> {
    this.pluginConsentReadmeError.set(null);
    this.pluginConsentReadmeLoadingId.set(requirement.pluginId);

    try {
      const readme = await this.pluginStore.loadRequirementReadme(requirement);

      this.pluginConsentReadme.set(readme);
    } catch (error) {
      this.pluginConsentReadmeError.set(error instanceof Error ? error.message : this.i18n.instant('servers.errors.loadPluginReadmeFailed'));
    } finally {
      this.pluginConsentReadmeLoadingId.set(null);
    }
  }

  closePluginConsentReadme(): void {
    this.pluginConsentReadme.set(null);
    this.pluginConsentReadmeError.set(null);
    this.pluginConsentReadmeLoadingId.set(null);
  }

  openPluginSource(requirement: PluginRequirementSummary): void {
    const sourceUrl = this.getPluginSourceUrl(requirement);

    if (sourceUrl) {
      this.externalLinks.open(sourceUrl);
    }
  }

  getPluginSourceUrl(requirement: PluginRequirementSummary): string | null {
    const candidate = requirement.manifest?.homepage ?? requirement.sourceUrl ?? requirement.installUrl ?? requirement.manifest?.bugs ?? null;

    return candidate?.startsWith('http://') || candidate?.startsWith('https://') ? candidate : null;
  }

  hasPluginReadme(requirement: PluginRequirementSummary): boolean {
    return !!requirement.manifest?.readme;
  }

  async confirmPasswordJoin(): Promise<void> {
    const server = this.passwordPromptServer();

    if (!server)
      return;

    await this.attemptJoinServer(server, this.joinPassword());
  }

  isServerMarkedBanned(server: ServerInfo): boolean {
    return !!this.bannedServerLookup()[server.id];
  }

  getServerUserCount(server: ServerInfo): number {
    const candidate = server as ServerInfo & { currentUsers?: number };

    if (typeof server.userCount === 'number')
      return server.userCount;

    return typeof candidate.currentUsers === 'number' ? candidate.currentUsers : 0;
  }

  getServerCapacityLabel(server: ServerInfo): string {
    return server.maxUsers > 0 ? String(server.maxUsers) : '∞';
  }

  getServerOwnerLabel(server: ServerInfo): string {
    const joinedRoom = this.joinedRoomForServer(server);
    const ownerKey = server.ownerId || joinedRoom?.hostId || '';
    const ownerMember = joinedRoom?.members?.find((member) => member.id === ownerKey || member.oderId === ownerKey);

    return server.ownerName || ownerMember?.displayName || server.ownerId || joinedRoom?.hostId || this.i18n.instant('servers.browser.ownerUnknown');
  }

  private openJoinedRoom(room: Room): void {
    this.joinedServerMenuId.set(null);
    this.store.dispatch(RoomsActions.viewServer({ room }));
  }

  private async attemptJoinServer(
    server: ServerInfo,
    password?: string,
    options: { acceptedRequirements?: PluginRequirementSummary[]; skipPluginConsent?: boolean } = {}
  ): Promise<void> {
    const currentUser = this.currentUser();
    const currentUserId = localStorage.getItem('metoyou_currentUserId') || currentUser?.id;

    if (!currentUserId) {
      this.router.navigate(['/login'], {
        queryParams: buildLoginReturnQueryParams(this.router.url)
      });

      return;
    }

    setStoredCurrentUserId(currentUserId);

    this.joinErrorMessage.set(null);
    this.joinPasswordError.set(null);

    try {
      if (options.skipPluginConsent !== true) {
        const consentDialog = await this.buildPluginConsentDialog(server, password);

        if (consentDialog) {
          this.pluginConsentDialog.set(consentDialog);
          this.selectedOptionalPluginIds.set(new Set(consentDialog.optional.map((requirement) => requirement.pluginId)));
          return;
        }
      }

      if (server.sourceUrl) {
        const hasCredential = await this.signalServerAuthorize.ensureCredentialForServerUrl(server.sourceUrl);

        if (!hasCredential) {
          return;
        }
      }

      const response = await firstValueFrom(
        this.serverDirectory.requestJoin(
          {
            roomId: server.id,
            userId: currentUserId,
            userPublicKey: currentUser?.oderId || currentUserId,
            displayName: currentUser?.displayName || this.i18n.instant('common.labels.anonymous'),
            password: password?.trim() || undefined
          },
          {
            sourceId: server.sourceId,
            sourceUrl: server.sourceUrl
          }
        )
      );
      const resolvedSource = this.serverDirectory.normaliseRoomSignalSource(
        {
          sourceId: response.server.sourceId ?? server.sourceId,
          sourceName: response.server.sourceName ?? server.sourceName,
          sourceUrl: response.server.sourceUrl ?? server.sourceUrl,
          signalingUrl: response.signalingUrl,
          fallbackName: response.server.sourceName ?? server.sourceName ?? server.name
        },
        {
          ensureEndpoint: true
        }
      );
      const resolvedServer = {
        ...server,
        ...response.server,
        channels: Array.isArray(response.server.channels) && response.server.channels.length > 0 ? response.server.channels : server.channels,
        ...resolvedSource,
        signalingUrl: response.signalingUrl
      };

      this.closePasswordDialog();

      if (options.acceptedRequirements?.length) {
        await this.pluginStore.installServerRequirementsLocally(resolvedServer.id, options.acceptedRequirements, { activate: true });
      }

      this.store.dispatch(
        RoomsActions.joinRoom({
          roomId: resolvedServer.id,
          serverInfo: resolvedServer
        })
      );
    } catch (error: unknown) {
      const serverError = error as {
        error?: { error?: string; errorCode?: string };
      };
      const errorCode = serverError?.error?.errorCode;
      const message = serverError?.error?.error || this.i18n.instant('servers.errors.joinFailed');

      if (errorCode === 'PASSWORD_REQUIRED') {
        this.passwordPromptServer.set(server);
        this.showPasswordDialog.set(true);
        this.joinPasswordError.set(message);
        return;
      }

      if (errorCode === 'BANNED') {
        this.bannedServerName.set(server.name);
        this.showBannedDialog.set(true);
        return;
      }

      this.joinErrorMessage.set(message);

      if (options.skipPluginConsent) {
        throw new Error(message);
      }
    }
  }

  private async buildPluginConsentDialog(server: ServerInfo, password?: string): Promise<JoinPluginConsentDialog | null> {
    const apiBaseUrl = this.serverDirectory.getApiBaseUrl({
      sourceId: server.sourceId,
      sourceUrl: server.sourceUrl
    });
    const snapshot = await firstValueFrom(this.pluginRequirements.getSnapshot(apiBaseUrl, server.id));
    const installedPluginIds = await this.pluginStore.getLocalServerInstalledPluginIds(server.id);
    const installableRequirements = snapshot.requirements
      .filter((requirement) => !installedPluginIds.has(requirement.pluginId))
      .filter((requirement) => !!requirement.manifest || !!requirement.installUrl);
    const required = installableRequirements.filter((requirement) => requirement.status === 'required');
    const optional = installableRequirements.filter(
      (requirement) => requirement.status === 'optional' || requirement.status === 'recommended'
    );

    if (required.length === 0 && optional.length === 0) {
      return null;
    }

    return {
      optional,
      password,
      required,
      server
    };
  }

  private async requestMissingServerIcons(servers: ServerInfo[], currentUser: User | null): Promise<void> {
    if (!currentUser) {
      return;
    }

    for (const server of servers) {
      if (server.icon) {
        continue;
      }

      const selector = this.serverDirectory.buildRoomSignalSelector(
        {
          sourceId: server.sourceId,
          sourceName: server.sourceName,
          sourceUrl: server.sourceUrl,
          fallbackName: server.sourceName ?? server.name
        },
        {
          ensureEndpoint: !!server.sourceUrl
        }
      );

      if (!selector) {
        continue;
      }

      const wsUrl = this.serverDirectory.getWebSocketUrl(selector);

      try {
        await firstValueFrom(this.webrtc.connectToSignalingServer(wsUrl));
        this.webrtc.identify(currentUser.oderId || currentUser.id, currentUser.displayName || this.i18n.instant('common.labels.user'), wsUrl, {
          description: currentUser.description,
          profileUpdatedAt: currentUser.profileUpdatedAt,
          homeSignalServerUrl: currentUser.homeSignalServerUrl
        });

        this.webrtc.sendRawMessageToSignalUrl(wsUrl, {
          type: 'server_icon_sync_request',
          serverId: server.id,
          iconUpdatedAt: 0
        });
      } catch {
        /* discovery icons are best-effort */
      }
    }
  }

  private async refreshBannedLookup(servers: ServerInfo[], currentUser: User | null): Promise<void> {
    const requestVersion = ++this.banLookupRequestVersion;

    if (!currentUser || servers.length === 0) {
      this.bannedServerLookup.set({});
      return;
    }

    const currentUserId = localStorage.getItem('metoyou_currentUserId');
    const entries = await Promise.all(
      servers.map(async (server) => {
        const bans = await this.db.getBansForRoom(server.id);
        const isBanned = hasRoomBanForUser(bans, currentUser, currentUserId);

        return [server.id, isBanned] as const;
      })
    );

    if (requestVersion !== this.banLookupRequestVersion)
      return;

    this.bannedServerLookup.set(Object.fromEntries(entries));
  }

  private async isServerBanned(server: ServerInfo): Promise<boolean> {
    const currentUser = this.currentUser();
    const currentUserId = localStorage.getItem('metoyou_currentUserId');

    if (!currentUser && !currentUserId)
      return false;

    const bans = await this.db.getBansForRoom(server.id);

    return hasRoomBanForUser(bans, currentUser, currentUserId);
  }
}
