/* eslint-disable @typescript-eslint/no-invalid-void-type */
import { Injectable, inject } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import {
  Observable,
  forkJoin,
  merge,
  of,
  throwError
} from 'rxjs';
import {
  catchError,
  map,
  scan
} from 'rxjs/operators';
import {
  ChannelPermissionOverride,
  type Channel,
  ROOM_PERMISSION_KEYS,
  RoomRole,
  RoomRoleAssignment,
  User
} from '../../../../shared-kernel';
import { ServerEndpointStateService } from '../../application/services/server-endpoint-state.service';
import type {
  BanServerMemberRequest,
  CreateServerInviteRequest,
  KickServerMemberRequest,
  ServerEndpoint,
  ServerInfo,
  ServerInviteInfo,
  ServerJoinAccessRequest,
  ServerJoinAccessResponse,
  ServerSourceSelector,
  UnbanServerMemberRequest
} from '../../domain/models/server-directory.model';
import type { RoomSignalSourceInput } from '../../domain/logic/room-signal-source.logic';
import { endpointSupportsServerDiscovery } from '../../domain/logic/server-discovery.rules';

interface ServerLookupError {
  status?: number;
  error?: {
    errorCode?: unknown;
  };
}

function isServerNotFoundError(error: unknown): boolean {
  const lookupError = error as ServerLookupError;

  return lookupError?.status === 404 && lookupError.error?.errorCode === 'SERVER_NOT_FOUND';
}

@Injectable({ providedIn: 'root' })
export class ServerDirectoryApiService {
  private readonly http = inject(HttpClient);
  private readonly endpointState = inject(ServerEndpointStateService);

  getApiBaseUrl(selector?: ServerSourceSelector): string {
    return `${this.resolveBaseServerUrl(selector)}/api`;
  }

  getWebSocketUrl(selector?: ServerSourceSelector): string {
    return this.resolveBaseServerUrl(selector).replace(/^http/, 'ws');
  }

  resolveEndpoint(selector?: ServerSourceSelector): ServerEndpoint | null {
    if (selector?.sourceId) {
      const endpoint = this.endpointState.servers().find((candidate) => candidate.id === selector.sourceId) ?? null;

      return this.endpointState.resolveCanonicalEndpoint(endpoint);
    }

    if (selector?.sourceUrl) {
      return this.endpointState.resolveCanonicalEndpoint(this.endpointState.findServerByUrl(selector.sourceUrl) ?? null);
    }

    return this.endpointState.resolveCanonicalEndpoint(
      this.endpointState.activeServer() ??
      this.endpointState.servers().find((endpoint) => endpoint.status !== 'incompatible') ??
      this.endpointState.servers()[0] ??
      null
    );
  }

  searchServers(query: string, shouldSearchAllServers: boolean): Observable<ServerInfo[]> {
    if (shouldSearchAllServers) {
      return this.searchAllEndpoints(query);
    }

    return this.searchSingleEndpoint(query, this.getApiBaseUrl(), this.endpointState.activeServer());
  }

  getServers(shouldSearchAllServers: boolean): Observable<ServerInfo[]> {
    if (shouldSearchAllServers) {
      return this.getAllServersFromAllEndpoints();
    }

    return this.http.get<{ servers: ServerInfo[]; total: number }>(`${this.getApiBaseUrl()}/servers`).pipe(
      map((response) => this.normalizeServerList(response, this.endpointState.activeServer())),
      catchError((error) => {
        console.error('Failed to get servers:', error);
        return of([]);
      })
    );
  }

  getFeaturedServers(limit?: number): Observable<ServerInfo[]> {
    return this.getDiscoveryServers('featured', limit);
  }

  getTrendingServers(limit?: number): Observable<ServerInfo[]> {
    return this.getDiscoveryServers('trending', limit);
  }

  getServer(serverId: string, selector?: ServerSourceSelector): Observable<ServerInfo | null> {
    return this.http.get<ServerInfo>(`${this.getApiBaseUrl(selector)}/servers/${serverId}`).pipe(
      map((server) => this.normalizeServerInfo(server, this.resolveEndpoint(selector))),
      catchError((error) => {
        // Warning: this API deliberately returns null for both authoritative
        // SERVER_NOT_FOUND and retryable endpoint failures. Callers that need
        // resilience must try other active endpoints before treating null as
        // proof that a room no longer exists.
        if (isServerNotFoundError(error)) {
          return of(null);
        }

        console.error('Failed to get server:', error);
        return of(null);
      })
    );
  }

  findServerAcrossActiveEndpoints(serverId: string, source?: RoomSignalSourceInput): Observable<ServerInfo | null> {
    const candidateEndpoints = this.getSearchableEndpoints();
    const preferredSourceUrl = source?.sourceUrl ? this.endpointState.sanitiseUrl(source.sourceUrl) : undefined;
    const prioritizedEndpoints = [...candidateEndpoints].sort((left, right) => {
      if (preferredSourceUrl) {
        const leftMatches = this.endpointState.sanitiseUrl(left.url) === preferredSourceUrl;
        const rightMatches = this.endpointState.sanitiseUrl(right.url) === preferredSourceUrl;

        if (leftMatches !== rightMatches) {
          return leftMatches ? -1 : 1;
        }
      }

      if (source?.sourceId) {
        const leftMatches = left.id === source.sourceId;
        const rightMatches = right.id === source.sourceId;

        if (leftMatches !== rightMatches) {
          return leftMatches ? -1 : 1;
        }
      }

      return 0;
    });

    if (prioritizedEndpoints.length === 0) {
      return this.getServer(serverId, this.resolveSelector(source));
    }

    return forkJoin(
      prioritizedEndpoints.map((endpoint) => this.getServer(serverId, {
        sourceId: endpoint.id,
        sourceUrl: endpoint.url
      }))
    ).pipe(
      map((servers) => servers.find((server): server is ServerInfo => !!server) ?? null)
    );
  }

  registerServer(
    server: Omit<ServerInfo, 'createdAt'> & { id?: string; password?: string | null },
    selector?: ServerSourceSelector
  ): Observable<ServerInfo> {
    return this.http.post<ServerInfo>(`${this.getApiBaseUrl(selector)}/servers`, server).pipe(
      catchError((error) => {
        console.error('Failed to register server:', error);
        return throwError(() => error);
      })
    );
  }

  updateServer(
    serverId: string,
    updates: Partial<ServerInfo> & {
      actingRole?: string;
      password?: string | null;
    },
    selector?: ServerSourceSelector
  ): Observable<ServerInfo> {
    return this.http.put<ServerInfo>(`${this.getApiBaseUrl(selector)}/servers/${serverId}`, updates).pipe(
      catchError((error) => {
        console.error('Failed to update server:', error);
        return throwError(() => error);
      })
    );
  }

  unregisterServer(serverId: string, selector?: ServerSourceSelector): Observable<void> {
    return this.http.delete<void>(`${this.getApiBaseUrl(selector)}/servers/${serverId}`).pipe(
      catchError((error) => {
        console.error('Failed to unregister server:', error);
        return throwError(() => error);
      })
    );
  }

  getServerUsers(serverId: string, selector?: ServerSourceSelector): Observable<User[]> {
    return this.http.get<User[]>(`${this.getApiBaseUrl(selector)}/servers/${serverId}/users`).pipe(
      catchError((error) => {
        console.error('Failed to get server users:', error);
        return of([]);
      })
    );
  }

  requestJoin(request: ServerJoinAccessRequest, selector?: ServerSourceSelector): Observable<ServerJoinAccessResponse> {
    return this.http.post<ServerJoinAccessResponse>(`${this.getApiBaseUrl(selector)}/servers/${request.roomId}/join`, request).pipe(
      catchError((error) => {
        console.error('Failed to send join request:', error);
        return throwError(() => error);
      })
    );
  }

  createInvite(serverId: string, request: CreateServerInviteRequest, selector?: ServerSourceSelector): Observable<ServerInviteInfo> {
    return this.http.post<ServerInviteInfo>(`${this.getApiBaseUrl(selector)}/servers/${serverId}/invites`, request).pipe(
      catchError((error) => {
        console.error('Failed to create invite:', error);
        return throwError(() => error);
      })
    );
  }

  getInvite(inviteId: string, selector?: ServerSourceSelector): Observable<ServerInviteInfo> {
    return this.http.get<ServerInviteInfo>(`${this.getApiBaseUrl(selector)}/invites/${inviteId}`).pipe(
      catchError((error) => {
        console.error('Failed to get invite:', error);
        return throwError(() => error);
      })
    );
  }

  kickServerMember(serverId: string, request: KickServerMemberRequest, selector?: ServerSourceSelector): Observable<void> {
    return this.http.post<void>(`${this.getApiBaseUrl(selector)}/servers/${serverId}/moderation/kick`, request).pipe(
      catchError((error) => {
        console.error('Failed to kick server member:', error);
        return throwError(() => error);
      })
    );
  }

  banServerMember(serverId: string, request: BanServerMemberRequest, selector?: ServerSourceSelector): Observable<void> {
    return this.http.post<void>(`${this.getApiBaseUrl(selector)}/servers/${serverId}/moderation/ban`, request).pipe(
      catchError((error) => {
        console.error('Failed to ban server member:', error);
        return throwError(() => error);
      })
    );
  }

  unbanServerMember(serverId: string, request: UnbanServerMemberRequest, selector?: ServerSourceSelector): Observable<void> {
    return this.http.post<void>(`${this.getApiBaseUrl(selector)}/servers/${serverId}/moderation/unban`, request).pipe(
      catchError((error) => {
        console.error('Failed to unban server member:', error);
        return throwError(() => error);
      })
    );
  }

  notifyLeave(serverId: string, userId: string, selector?: ServerSourceSelector): Observable<void> {
    return this.http.post<void>(`${this.getApiBaseUrl(selector)}/servers/${serverId}/leave`, { userId }).pipe(
      catchError((error) => {
        console.error('Failed to notify leave:', error);
        return of(undefined);
      })
    );
  }

  updateUserCount(serverId: string, count: number): Observable<void> {
    return this.http.patch<void>(`${this.getApiBaseUrl()}/servers/${serverId}/user-count`, { count }).pipe(
      catchError((error) => {
        console.error('Failed to update user count:', error);
        return of(undefined);
      })
    );
  }

  sendHeartbeat(serverId: string): Observable<void> {
    return this.http.post<void>(`${this.getApiBaseUrl()}/servers/${serverId}/heartbeat`, {}).pipe(
      catchError((error) => {
        console.error('Failed to send heartbeat:', error);
        return of(undefined);
      })
    );
  }

  private getDiscoveryServers(kind: 'featured' | 'trending', limit?: number): Observable<ServerInfo[]> {
    const baseUrl = this.resolveBaseServerUrl();

    if (!endpointSupportsServerDiscovery(baseUrl)) {
      return of([]);
    }

    const params = typeof limit === 'number' ? new HttpParams().set('limit', String(limit)) : undefined;

    return this.http
      .get<{ servers: ServerInfo[]; total: number }>(`${this.getApiBaseUrl()}/servers/${kind}`, params ? { params } : {})
      .pipe(
        map((response) => this.normalizeServerList(response, this.endpointState.activeServer())),
        catchError((error) => {
          console.error(`Failed to get ${kind} servers:`, error);
          return of([]);
        })
      );
  }

  private resolveBaseServerUrl(selector?: ServerSourceSelector): string {
    const resolvedEndpoint = this.resolveEndpoint(selector);

    if (resolvedEndpoint) {
      return resolvedEndpoint.url;
    }

    if (selector?.sourceUrl) {
      return this.endpointState.sanitiseUrl(selector.sourceUrl);
    }

    return this.endpointState.getPrimaryDefaultServerUrl();
  }

  private unwrapServersResponse(response: { servers: ServerInfo[]; total: number } | ServerInfo[]): ServerInfo[] {
    return Array.isArray(response) ? response : (response.servers ?? []);
  }

  private searchSingleEndpoint(query: string, apiBaseUrl: string, source?: ServerEndpoint | null): Observable<ServerInfo[]> {
    const params = new HttpParams().set('q', query);

    return this.http.get<{ servers: ServerInfo[]; total: number }>(`${apiBaseUrl}/servers`, { params }).pipe(
      map((response) => this.normalizeServerList(response, source)),
      catchError((error) => {
        console.error('Failed to search servers:', error);
        return of([]);
      })
    );
  }

  private searchAllEndpoints(query: string): Observable<ServerInfo[]> {
    const onlineEndpoints = this.getSearchableEndpoints();

    if (onlineEndpoints.length === 0) {
      return this.searchSingleEndpoint(query, this.getApiBaseUrl(), this.endpointState.activeServer());
    }

    return merge(...onlineEndpoints.map((endpoint) => this.searchSingleEndpoint(query, `${endpoint.url}/api`, endpoint))).pipe(
      scan((servers, endpointServers) => this.deduplicateById([...servers, ...endpointServers]), [] as ServerInfo[])
    );
  }

  private getAllServersFromAllEndpoints(): Observable<ServerInfo[]> {
    const onlineEndpoints = this.getSearchableEndpoints();

    if (onlineEndpoints.length === 0) {
      return this.http.get<{ servers: ServerInfo[]; total: number }>(`${this.getApiBaseUrl()}/servers`).pipe(
        map((response) => this.normalizeServerList(response, this.endpointState.activeServer())),
        catchError(() => of([]))
      );
    }

    return forkJoin(
      onlineEndpoints.map((endpoint) =>
        this.http.get<{ servers: ServerInfo[]; total: number }>(`${endpoint.url}/api/servers`).pipe(
          map((response) => this.normalizeServerList(response, endpoint)),
          catchError(() => of([] as ServerInfo[]))
        )
      )
    ).pipe(map((resultArrays) => resultArrays.flat()));
  }

  private getSearchableEndpoints(): ServerEndpoint[] {
    const activeEndpoints = this.endpointState.activeServers().filter((endpoint) => endpoint.status !== 'offline');

    if (activeEndpoints.length > 0) {
      return activeEndpoints;
    }

    const activeServer = this.endpointState.activeServer();

    return activeServer ? [activeServer] : [];
  }

  private resolveSelector(source?: RoomSignalSourceInput): ServerSourceSelector | undefined {
    if (source?.sourceId) {
      return { sourceId: source.sourceId };
    }

    if (source?.sourceUrl) {
      return { sourceUrl: source.sourceUrl };
    }

    return undefined;
  }

  private deduplicateById<T extends { id: string }>(items: T[]): T[] {
    const seen = new Set<string>();

    return items.filter((item) => {
      if (seen.has(item.id)) {
        return false;
      }

      seen.add(item.id);
      return true;
    });
  }

  private normalizeServerList(response: { servers: ServerInfo[]; total: number } | ServerInfo[], source?: ServerEndpoint | null): ServerInfo[] {
    return this.unwrapServersResponse(response).map((server) => this.normalizeServerInfo(server, source));
  }

  private normalizeServerInfo(server: ServerInfo | Record<string, unknown>, source?: ServerEndpoint | null): ServerInfo {
    const candidate = server as Record<string, unknown>;
    const sourceName = this.getStringValue(candidate['sourceName']);
    const sourceUrl = this.getStringValue(candidate['sourceUrl']);
    const resolvedSource = this.endpointState.resolveCanonicalEndpoint(source);

    return {
      id: this.getStringValue(candidate['id']) ?? '',
      name: this.getStringValue(candidate['name']) ?? 'Unnamed server',
      description: this.getStringValue(candidate['description']),
      topic: this.getStringValue(candidate['topic']),
      hostName: this.getStringValue(candidate['hostName']) ?? sourceName ?? resolvedSource?.name ?? 'Unknown API',
      ownerId: this.getStringValue(candidate['ownerId']),
      ownerName: this.getStringValue(candidate['ownerName']),
      ownerPublicKey: this.getStringValue(candidate['ownerPublicKey']),
      userCount: this.getNumberValue(candidate['userCount'], this.getNumberValue(candidate['currentUsers'])),
      maxUsers: this.getNumberValue(candidate['maxUsers']),
      hasPassword: this.getBooleanValue(candidate['hasPassword']),
      isPrivate: this.getBooleanValue(candidate['isPrivate']),
      tags: Array.isArray(candidate['tags']) ? (candidate['tags'] as string[]) : [],
      channels: this.getChannelsValue(candidate['channels']),
      slowModeInterval: this.getNumberValue(candidate['slowModeInterval'], 0),
      roles: this.getRolesValue(candidate['roles']),
      roleAssignments: this.getRoleAssignmentsValue(candidate['roleAssignments']),
      channelPermissions: this.getChannelPermissionOverridesValue(candidate['channelPermissions']),
      createdAt: this.getNumberValue(candidate['createdAt'], Date.now()),
      sourceId: this.getStringValue(candidate['sourceId']) ?? resolvedSource?.id,
      sourceName: sourceName ?? resolvedSource?.name,
      sourceUrl: sourceUrl
        ? this.endpointState.sanitiseUrl(sourceUrl)
        : resolvedSource
          ? this.endpointState.sanitiseUrl(resolvedSource.url)
          : undefined
    };
  }

  private getBooleanValue(value: unknown): boolean {
    return typeof value === 'boolean' ? value : value === 1;
  }

  private getNumberValue(value: unknown, fallback = 0): number {
    return typeof value === 'number' ? value : fallback;
  }

  private getChannelsValue(value: unknown): Channel[] | undefined {
    if (!Array.isArray(value)) {
      return undefined;
    }

    return value
      .filter((channel): channel is Record<string, unknown> => !!channel && typeof channel === 'object')
      .map((channel, index) => {
        const id = this.getStringValue(channel['id']);
        const name = this.getStringValue(channel['name']);
        const type = this.getChannelTypeValue(channel['type']);
        const position = this.getNumberValue(channel['position'], index);

        if (!id || !name || !type) {
          return null;
        }

        return {
          id,
          name,
          type,
          position
        } satisfies Channel;
      })
      .filter((channel): channel is Channel => !!channel);
  }

  private getRolesValue(value: unknown): RoomRole[] | undefined {
    if (!Array.isArray(value)) {
      return undefined;
    }

    return value
      .filter((role): role is Record<string, unknown> => !!role && typeof role === 'object')
      .flatMap((role, index) => {
        const id = this.getStringValue(role['id']);
        const name = this.getStringValue(role['name']);
        const position = this.getNumberValue(role['position'], index * 100);

        if (!id || !name) {
          return [];
        }

        const normalizedRole: RoomRole = {
          id,
          name,
          position,
          permissions: this.getPermissionMatrixValue(role['permissions'])
        };
        const color = this.getStringValue(role['color']);
        const isSystem = typeof role['isSystem'] === 'boolean' ? role['isSystem'] : this.getBooleanValue(role['isSystem']);

        if (color) {
          normalizedRole.color = color;
        }

        if (typeof isSystem === 'boolean') {
          normalizedRole.isSystem = isSystem;
        }

        return [normalizedRole];
      });
  }

  private getRoleAssignmentsValue(value: unknown): RoomRoleAssignment[] | undefined {
    if (!Array.isArray(value)) {
      return undefined;
    }

    return value
      .filter((assignment): assignment is Record<string, unknown> => !!assignment && typeof assignment === 'object')
      .flatMap((assignment) => {
        const userId = this.getStringValue(assignment['userId']);
        const roleIds = Array.isArray(assignment['roleIds'])
          ? assignment['roleIds'].filter((roleId): roleId is string => typeof roleId === 'string')
          : [];

        if (!userId || roleIds.length === 0) {
          return [];
        }

        const normalizedAssignment: RoomRoleAssignment = {
          userId,
          roleIds
        };
        const oderId = this.getStringValue(assignment['oderId']);

        if (oderId) {
          normalizedAssignment.oderId = oderId;
        }

        return [normalizedAssignment];
      });
  }

  private getChannelPermissionOverridesValue(value: unknown): ChannelPermissionOverride[] | undefined {
    if (!Array.isArray(value)) {
      return undefined;
    }

    return value
      .filter((override): override is Record<string, unknown> => !!override && typeof override === 'object')
      .map((override) => {
        const channelId = this.getStringValue(override['channelId']);
        const targetId = this.getStringValue(override['targetId']);
        const targetType = override['targetType'] === 'role' || override['targetType'] === 'user' ? override['targetType'] : undefined;
        const permission = ROOM_PERMISSION_KEYS.find((candidatePermission) => candidatePermission === override['permission']);
        const valueState =
          override['value'] === 'allow' || override['value'] === 'deny' || override['value'] === 'inherit' ? override['value'] : undefined;

        if (!channelId || !targetId || !targetType || !permission || !valueState) {
          return null;
        }

        return {
          channelId,
          targetId,
          targetType,
          permission,
          value: valueState
        } satisfies ChannelPermissionOverride;
      })
      .filter((override): override is ChannelPermissionOverride => !!override);
  }

  private getPermissionMatrixValue(value: unknown): RoomRole['permissions'] {
    if (!value || typeof value !== 'object') {
      return undefined;
    }

    const matrix = value as Record<string, unknown>;
    const normalized = ROOM_PERMISSION_KEYS.reduce<NonNullable<RoomRole['permissions']>>((nextMatrix, permission) => {
      const permissionValue = matrix[permission];

      if (permissionValue === 'allow' || permissionValue === 'deny' || permissionValue === 'inherit') {
        nextMatrix[permission] = permissionValue;
      }

      return nextMatrix;
    }, {});

    return Object.keys(normalized).length > 0 ? normalized : undefined;
  }

  private getChannelTypeValue(value: unknown): Channel['type'] | undefined {
    return value === 'text' || value === 'voice' ? value : undefined;
  }

  private getStringValue(value: unknown): string | undefined {
    return typeof value === 'string' ? value : undefined;
  }
}
