import {
  expect,
  type Page,
  type Locator
} from '@playwright/test';

export class ChatRoomPage {
  readonly chatMessages: Locator;
  readonly voiceWorkspace: Locator;
  readonly channelsSidePanel: Locator;
  readonly usersSidePanel: Locator;

  constructor(private page: Page) {
    this.chatMessages = page.locator('app-chat-messages');
    this.voiceWorkspace = page.locator('app-voice-workspace');
    this.channelsSidePanel = page.locator('app-rooms-side-panel').first();
    this.usersSidePanel = page.locator('app-rooms-side-panel').last();
  }

  /** Click a voice channel by name in the channels sidebar to join voice. */
  async joinVoiceChannel(channelName: string) {
    const channelButton = this.getVoiceChannelButton(channelName);

    if (await channelButton.count() === 0) {
      await this.refreshRoomMetadata();
    }

    if (await channelButton.count() === 0) {
      // Second attempt - metadata might still be syncing
      await this.page.waitForTimeout(2_000);
      await this.refreshRoomMetadata();
    }

    await expect(channelButton).toBeVisible({ timeout: 15_000 });
    await channelButton.click();
  }

  /** Creates a voice channel if it is not already present in the current room. */
  async ensureVoiceChannelExists(channelName: string) {
    const channelButton = this.getVoiceChannelButton(channelName);

    if (await channelButton.count() > 0) {
      return;
    }

    await this.refreshRoomMetadata();

    // Wait a bit longer for Angular to render the channel list after refresh
    try {
      await expect(channelButton).toBeVisible({ timeout: 5_000 });
      return;
    } catch {
      // Channel genuinely doesn't exist - create it
    }

    await this.openCreateVoiceChannelDialog();

    try {
      await this.createChannel(channelName);
    } catch {
      // If the dialog didn't close (e.g. duplicate name validation), dismiss it
      const dialog = this.page.locator('app-confirm-dialog');

      if (await dialog.isVisible()) {
        const cancelButton = dialog.getByRole('button', { name: 'Cancel' });
        const closeButton = dialog.getByRole('button', { name: 'Close dialog' });

        if (await cancelButton.isVisible()) {
          await cancelButton.click();
        } else if (await closeButton.isVisible()) {
          await closeButton.click();
        }

        await expect(dialog).not.toBeVisible({ timeout: 5_000 }).catch(() => {});
      }
    }

    await expect(channelButton).toBeVisible({ timeout: 15_000 });
  }

  /** Click a text channel by name in the channels sidebar to switch chat rooms. */
  async joinTextChannel(channelName: string) {
    const channelButton = this.getTextChannelButton(channelName);

    if (await channelButton.count() === 0) {
      await this.refreshRoomMetadata();
    }

    await expect(channelButton).toBeVisible({ timeout: 15_000 });
    await channelButton.click();
  }

  /** Creates a text channel and waits until it appears locally. */
  async ensureTextChannelExists(channelName: string) {
    const channelButton = this.getTextChannelButton(channelName);

    if (await channelButton.count() > 0) {
      return;
    }

    await this.openCreateTextChannelDialog();
    await this.createChannel(channelName);

    try {
      await expect(channelButton).toBeVisible({ timeout: 5_000 });
    } catch {
      await this.createTextChannelThroughComponent(channelName);
    }

    await this.persistCurrentChannelsToServer(channelName);
    await expect(channelButton).toBeVisible({ timeout: 15_000 });
  }

  /** Click "Create Voice Channel" button in the channels sidebar. */
  async openCreateVoiceChannelDialog() {
    await this.page.locator('button[title="Create Voice Channel"]').click();
  }

  /** Click "Create Text Channel" button in the channels sidebar. */
  async openCreateTextChannelDialog() {
    await this.page.locator('button[title="Create Text Channel"]').click();
  }

  /** Fill the channel name in the create channel dialog and confirm. */
  async createChannel(name: string) {
    const dialog = this.page.locator('app-confirm-dialog');
    const channelNameInput = dialog.getByPlaceholder('Channel name');
    const createButton = dialog.getByRole('button', { name: 'Create', exact: true });

    await expect(channelNameInput).toBeVisible({ timeout: 10_000 });
    await channelNameInput.fill(name);
    await channelNameInput.press('Enter');

    if (await dialog.isVisible()) {
      try {
        await createButton.click();
      } catch {
        // Enter may already have confirmed and removed the dialog.
      }
    }

    await expect(dialog).not.toBeVisible({ timeout: 10_000 });
  }

  /** Get the voice controls component. */
  get voiceControls() {
    return this.page.locator('app-voice-controls');
  }

  /** Get the mute toggle button inside voice controls. */
  get muteButton() {
    return this.voiceControls.locator('button:has(ng-icon[name="lucideMic"]), button:has(ng-icon[name="lucideMicOff"])').first();
  }

  /** Get the deafen toggle button inside voice controls. */
  get deafenButton() {
    return this.voiceControls.locator('button:has(ng-icon[name="lucideHeadphones"])').first();
  }

  /** Get the disconnect/hang-up button (destructive styled). */
  get disconnectButton() {
    return this.voiceControls.locator('button:has(ng-icon[name="lucidePhoneOff"])').first();
  }

  /** Get all voice stream tiles. */
  get streamTiles() {
    return this.page.locator('app-voice-workspace-stream-tile');
  }

  /** Get the count of voice users listed under a voice channel. */
  async getVoiceUserCountInChannel(channelName: string): Promise<number> {
    // The voice channel button is inside a wrapper div; user avatars are siblings within that wrapper
    const channelWrapper = this.getVoiceChannelButton(channelName).locator('xpath=ancestor::div[1]');
    const userAvatars = channelWrapper.locator('app-user-avatar');

    return userAvatars.count();
  }

  /** Get the screen share toggle button inside voice controls. */
  get screenShareButton() {
    return this.voiceControls.locator(
      'button:has(ng-icon[name="lucideMonitor"]), button:has(ng-icon[name="lucideMonitorOff"])'
    ).first();
  }

  /** Start screen sharing.  Bypasses the quality dialog via localStorage preset. */
  async startScreenShare() {
    // Disable quality dialog so clicking the button starts sharing immediately
    await this.page.evaluate(() => {
      const key = 'metoyou_voice_settings';
      const raw = localStorage.getItem(key);
      const settings = raw ? JSON.parse(raw) : {};

      settings.askScreenShareQuality = false;
      settings.screenShareQuality = 'balanced';
      localStorage.setItem(key, JSON.stringify(settings));
    });

    await this.screenShareButton.click();
  }

  /** Stop screen sharing by clicking the active screen share button. */
  async stopScreenShare() {
    await this.screenShareButton.click();
  }

  /** Check whether the screen share button shows the active (MonitorOff) icon. */
  get isScreenShareActive() {
    return this.voiceControls.locator('button:has(ng-icon[name="lucideMonitorOff"])').first();
  }

  private getTextChannelButton(channelName: string): Locator {
    return this.channelsSidePanel.locator(`button[data-channel-type="text"][data-channel-name="${channelName}"]`).first();
  }

  private getVoiceChannelButton(channelName: string): Locator {
    return this.channelsSidePanel.locator(`button[data-channel-type="voice"][data-channel-name="${channelName}"]`).first();
  }

  private async createTextChannelThroughComponent(channelName: string): Promise<void> {
    await this.page.evaluate((name) => {
      interface ChannelSidebarComponent {
        createChannel: (type: 'text' | 'voice') => void;
        newChannelName: string;
        confirmCreateChannel: () => void;
      }
      interface AngularDebugApi {
        getComponent: (element: Element) => ChannelSidebarComponent;
      }
      interface WindowWithAngularDebug extends Window {
        ng?: AngularDebugApi;
      }

      const host = document.querySelector('app-rooms-side-panel');
      const debugApi = (window as WindowWithAngularDebug).ng;

      if (!host || !debugApi?.getComponent) {
        throw new Error('Angular debug API unavailable for text channel fallback');
      }

      const component = debugApi.getComponent(host);

      component.createChannel('text');
      component.newChannelName = name;
      component.confirmCreateChannel();
    }, channelName);
  }

  private async persistCurrentChannelsToServer(channelName: string): Promise<void> {
    const result = await this.page.evaluate(async (requestedChannelName) => {
      interface ServerEndpoint {
        isActive?: boolean;
        url: string;
      }

      interface ChannelShape {
        id: string;
        name: string;
        type: 'text' | 'voice';
        position: number;
      }

      interface RoomShape {
        id: string;
        sourceUrl?: string;
        channels?: ChannelShape[];
      }

      interface UserShape {
        id: string;
      }

      interface ChannelSidebarComponent {
        currentRoom: () => RoomShape | null;
        currentUser: () => UserShape | null;
      }

      interface AngularDebugApi {
        getComponent: (element: Element) => ChannelSidebarComponent;
      }

      interface WindowWithAngularDebug extends Window {
        ng?: AngularDebugApi;
      }

      const host = document.querySelector('app-rooms-side-panel');
      const debugApi = (window as WindowWithAngularDebug).ng;

      if (!host || !debugApi?.getComponent) {
        throw new Error('Angular debug API unavailable for channel persistence');
      }

      const component = debugApi.getComponent(host);
      const room = component.currentRoom();
      const currentUser = component.currentUser();
      const endpoints = JSON.parse(localStorage.getItem('metoyou_server_endpoints') || '[]') as ServerEndpoint[];
      const activeEndpoint = endpoints.find((endpoint) => endpoint.isActive) || endpoints[0] || null;
      const apiBaseUrl = room?.sourceUrl || activeEndpoint?.url;
      const normalizedChannelName = requestedChannelName.trim().replace(/\s+/g, ' ');
      const existingChannels = Array.isArray(room?.channels) ? room.channels : [];
      const hasTextChannel = existingChannels.some((channel) =>
        channel.type === 'text' && channel.name.trim().toLowerCase() === normalizedChannelName.toLowerCase()
      );
      const nextChannels = hasTextChannel
        ? existingChannels
        : [
          ...existingChannels,
          {
            id: globalThis.crypto.randomUUID(),
            name: normalizedChannelName,
            type: 'text' as const,
            position: existingChannels.length
          }
        ];

      if (!room?.id || !currentUser?.id || !apiBaseUrl) {
        throw new Error('Missing room, user, or endpoint when persisting channels');
      }

      const response = await fetch(`${apiBaseUrl}/api/servers/${room.id}`, {
        method: 'PUT',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({
          currentOwnerId: currentUser.id,
          channels: nextChannels
        })
      });

      if (!response.ok) {
        throw new Error(`Failed to persist channels: ${response.status}`);
      }

      return { roomId: room.id, channels: nextChannels };
    }, channelName);

    // Update NGRX store directly so the UI reflects the new channel
    // immediately, without waiting for an async effect round-trip.
    await this.dispatchRoomChannelsUpdate(result.roomId, result.channels);
  }

  private async dispatchRoomChannelsUpdate(
    roomId: string,
    channels: { id: string; name: string; type: string; position: number }[]
  ): Promise<void> {
    await this.page.evaluate(({ rid, chs }) => {
      interface AngularDebugApi {
        getComponent: (element: Element) => Record<string, unknown>;
      }

      const host = document.querySelector('app-rooms-side-panel');
      const debugApi = (window as { ng?: AngularDebugApi }).ng;

      if (!host || !debugApi?.getComponent) {
        return;
      }

      const component = debugApi.getComponent(host);
      const store = component['store'] as { dispatch: (a: Record<string, unknown>) => void } | undefined;

      if (store?.dispatch) {
        store.dispatch({
          type: '[Rooms] Update Room',
          roomId: rid,
          changes: { channels: chs }
        });
      }
    }, { rid: roomId, chs: channels });
  }

  private async refreshRoomMetadata(): Promise<void> {
    await this.page.evaluate(async () => {
      interface ServerEndpoint {
        isActive?: boolean;
        url: string;
      }

      interface ChannelShape {
        id: string;
        name: string;
        type: 'text' | 'voice';
        position: number;
      }

      interface AngularDebugApi {
        getComponent: (element: Element) => Record<string, unknown>;
      }

      interface WindowWithAngularDebug extends Window {
        ng?: AngularDebugApi;
      }

      const host = document.querySelector('app-rooms-side-panel');
      const debugApi = (window as WindowWithAngularDebug).ng;

      if (!host || !debugApi?.getComponent) {
        throw new Error('Angular debug API unavailable for room refresh');
      }

      const component = debugApi.getComponent(host);
      const currentRoom = typeof component['currentRoom'] === 'function'
        ? (component['currentRoom'] as () => { id: string; sourceUrl?: string; channels?: ChannelShape[] } | null)()
        : null;

      if (!currentRoom) {
        throw new Error('No current room to refresh');
      }

      const store = component['store'] as { dispatch: (action: Record<string, unknown>) => void } | undefined;

      if (!store?.dispatch) {
        throw new Error('NGRX store not available on component');
      }

      // Fetch server data directly via REST API instead of triggering
      // an async NGRX effect that can race with pending writes.
      const endpoints = JSON.parse(localStorage.getItem('metoyou_server_endpoints') || '[]') as ServerEndpoint[];
      const activeEndpoint = endpoints.find((ep) => ep.isActive) || endpoints[0] || null;
      const apiBaseUrl = currentRoom.sourceUrl || activeEndpoint?.url;

      if (!apiBaseUrl) {
        throw new Error('No API base URL available for room refresh');
      }

      const response = await fetch(`${apiBaseUrl}/api/servers/${currentRoom.id}`);

      if (response.ok) {
        const serverData = await response.json() as { channels?: ChannelShape[] };

        if (serverData.channels?.length) {
          store.dispatch({
            type: '[Rooms] Update Room',
            roomId: currentRoom.id,
            changes: { channels: serverData.channels }
          });
        }
      }
    });

    // Brief wait for Angular change detection to propagate
    await this.page.waitForTimeout(500);
  }
}
