import {
  Injectable,
  inject,
  type Signal
} from '@angular/core';
import { Observable } from 'rxjs';
import { STORAGE_KEY_CONNECTION_SETTINGS } from '../../../../core/constants';
import { User } from '../../../../shared-kernel';
import { ServerDirectoryApiService } from '../../infrastructure/services/server-directory-api.service';
import type {
  BanServerMemberRequest,
  CreateServerInviteRequest,
  KickServerMemberRequest,
  ServerEndpoint,
  ServerEndpointVersions,
  ServerInfo,
  ServerInviteInfo,
  ServerJoinAccessRequest,
  ServerJoinAccessResponse,
  ServerSourceSelector,
  UnbanServerMemberRequest
} from '../../domain/models/server-directory.model';
import {
  buildRoomSignalSelector,
  buildRoomSignalSource,
  type RoomSignalSource,
  type RoomSignalSourceInput
} from '../../domain/logic/room-signal-source.logic';
import { ServerEndpointCompatibilityService } from '../../infrastructure/services/server-endpoint-compatibility.service';
import { ServerEndpointHealthService } from '../../infrastructure/services/server-endpoint-health.service';
import { ServerEndpointStateService } from './server-endpoint-state.service';
import { SignalServerAuthService } from '../../../authentication/application/services/signal-server-auth.service';

@Injectable({ providedIn: 'root' })
export class ServerDirectoryService {
  readonly servers: Signal<ServerEndpoint[]>;
  readonly activeServers: Signal<ServerEndpoint[]>;
  readonly hasMissingDefaultServers: Signal<boolean>;
  readonly activeServer: Signal<ServerEndpoint | null>;

  private readonly endpointState = inject(ServerEndpointStateService);
  private readonly endpointCompatibility = inject(ServerEndpointCompatibilityService);
  private readonly endpointHealth = inject(ServerEndpointHealthService);
  private readonly api = inject(ServerDirectoryApiService);
  private readonly signalServerAuth = inject(SignalServerAuthService);
  private readonly initialServerHealthCheck: Promise<void>;
  private shouldSearchAllServers = true;

  constructor() {
    this.servers = this.endpointState.servers;
    this.activeServers = this.endpointState.activeServers;
    this.hasMissingDefaultServers = this.endpointState.hasMissingDefaultServers;
    this.activeServer = this.endpointState.activeServer;

    this.loadConnectionSettings();
    this.initialServerHealthCheck = this.testAllServers().catch(() => undefined);
  }

  async awaitInitialServerHealthCheck(): Promise<void> {
    await this.initialServerHealthCheck;
  }

  addServer(server: { name: string; url: string }): ServerEndpoint {
    return this.endpointState.addServer(server);
  }

  ensureServerEndpoint(
    server: { name: string; url: string },
    options?: { setActive?: boolean }
  ): ServerEndpoint {
    return this.endpointState.ensureServerEndpoint(server, options);
  }

  findServerByUrl(url: string): ServerEndpoint | undefined {
    return this.endpointState.findServerByUrl(url);
  }

  removeServer(endpointId: string): void {
    this.endpointState.removeServer(endpointId);
  }

  restoreDefaultServers(): ServerEndpoint[] {
    return this.endpointState.restoreDefaultServers();
  }

  setActiveServer(endpointId: string): void {
    this.endpointState.setActiveServer(endpointId);
  }

  deactivateServer(endpointId: string): void {
    this.endpointState.deactivateServer(endpointId);
  }

  updateServerStatus(
    endpointId: string,
    status: ServerEndpoint['status'],
    latency?: number,
    versions?: ServerEndpointVersions,
    serverTag?: string
  ): void {
    this.endpointState.updateServerStatus(endpointId, status, latency, versions, serverTag);
  }

  async ensureEndpointVersionCompatibility(selector?: ServerSourceSelector): Promise<boolean> {
    const endpoint = this.api.resolveEndpoint(selector);

    if (!endpoint || endpoint.status === 'incompatible') {
      return false;
    }

    const clientVersion = await this.endpointCompatibility.getClientVersion();

    if (!clientVersion) {
      return true;
    }

    await this.testServer(endpoint.id);

    const refreshedEndpoint = this.servers().find((candidate) => candidate.id === endpoint.id);

    return !!refreshedEndpoint && refreshedEndpoint.status !== 'incompatible';
  }

  resolveRoomEndpoint(
    source?: RoomSignalSourceInput,
    options?: { ensureEndpoint?: boolean; setActive?: boolean }
  ): ServerEndpoint | null {
    const normalizedSource = buildRoomSignalSource(source);

    if (normalizedSource.sourceId) {
      const endpointById = this.endpointState.resolveCanonicalEndpoint(
        this.servers().find((endpoint) => endpoint.id === normalizedSource.sourceId) ?? null
      );

      if (endpointById) {
        return endpointById;
      }
    }

    if (!normalizedSource.sourceUrl) {
      return null;
    }

    const endpointByUrl = this.endpointState.resolveCanonicalEndpoint(
      this.findServerByUrl(normalizedSource.sourceUrl) ?? null
    );

    if (endpointByUrl) {
      return endpointByUrl;
    }

    if (!options?.ensureEndpoint) {
      return null;
    }

    return this.ensureServerEndpoint({
      name: normalizedSource.sourceName ?? 'Signal Server',
      url: normalizedSource.sourceUrl
    }, {
      setActive: options.setActive
    });
  }

  normaliseRoomSignalSource(
    source?: RoomSignalSourceInput,
    options?: { ensureEndpoint?: boolean; setActive?: boolean }
  ): RoomSignalSource {
    const endpoint = this.resolveRoomEndpoint(source, options);

    return buildRoomSignalSource(source, endpoint);
  }

  buildRoomSignalSelector(
    source?: RoomSignalSourceInput,
    options?: { ensureEndpoint?: boolean; setActive?: boolean }
  ): ServerSourceSelector | undefined {
    return buildRoomSignalSelector(this.normaliseRoomSignalSource(source, options));
  }

  getFallbackRoomEndpoints(source?: RoomSignalSourceInput): ServerEndpoint[] {
    const primaryEndpoint = this.resolveRoomEndpoint(source);
    const primarySource = this.normaliseRoomSignalSource(source);
    const primaryUrl = primarySource.sourceUrl ? this.endpointState.sanitiseUrl(primarySource.sourceUrl) : null;
    const primaryInstanceId = primaryEndpoint?.instanceId ?? null;

    return this.activeServers().filter((endpoint) => {
      if (endpoint.status === 'incompatible') {
        return false;
      }

      if (primaryInstanceId && endpoint.instanceId === primaryInstanceId) {
        return false;
      }

      return !primaryUrl || this.endpointState.sanitiseUrl(endpoint.url) !== primaryUrl;
    });
  }

  setSearchAllServers(enabled: boolean): void {
    this.shouldSearchAllServers = enabled;
  }

  async testServer(endpointId: string): Promise<boolean> {
    const endpoint = this.servers().find((entry) => entry.id === endpointId);

    if (!endpoint) {
      return false;
    }

    this.updateServerStatus(endpointId, 'checking');
    const clientVersion = await this.endpointCompatibility.getClientVersion();
    const healthResult = await this.endpointHealth.probeEndpoint(endpoint, clientVersion);

    this.updateServerStatus(
      endpointId,
      healthResult.status,
      healthResult.latency,
      healthResult.versions,
      healthResult.serverTag
    );

    if (healthResult.status === 'online' && endpoint.isActive) {
      void this.signalServerAuth.ensureProvisioned(endpoint.url).catch(() => undefined);
    }

    return healthResult.status === 'online';
  }

  async testAllServers(): Promise<void> {
    await Promise.all(this.servers().map((endpoint) => this.testServer(endpoint.id)));
  }

  getApiBaseUrl(selector?: ServerSourceSelector): string {
    return this.api.getApiBaseUrl(selector);
  }

  getWebSocketUrl(selector?: ServerSourceSelector): string {
    return this.api.getWebSocketUrl(selector);
  }

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

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

  getFeaturedServers(limit?: number): Observable<ServerInfo[]> {
    return this.api.getFeaturedServers(limit);
  }

  getTrendingServers(limit?: number): Observable<ServerInfo[]> {
    return this.api.getTrendingServers(limit);
  }

  getServer(serverId: string, selector?: ServerSourceSelector): Observable<ServerInfo | null> {
    return this.api.getServer(serverId, selector);
  }

  findServerAcrossActiveEndpoints(serverId: string, source?: RoomSignalSourceInput): Observable<ServerInfo | null> {
    return this.api.findServerAcrossActiveEndpoints(serverId, source);
  }

  registerServer(
    server: Omit<ServerInfo, 'createdAt'> & { id?: string; password?: string | null },
    selector?: ServerSourceSelector
  ): Observable<ServerInfo> {
    return this.api.registerServer(server, selector);
  }

  updateServer(
    serverId: string,
    updates: Partial<ServerInfo> & {
      actingRole?: string;
      password?: string | null;
    },
    selector?: ServerSourceSelector
  ): Observable<ServerInfo> {
    return this.api.updateServer(serverId, updates, selector);
  }

  unregisterServer(serverId: string, selector?: ServerSourceSelector): Observable<void> {
    return this.api.unregisterServer(serverId, selector);
  }

  getServerUsers(serverId: string, selector?: ServerSourceSelector): Observable<User[]> {
    return this.api.getServerUsers(serverId, selector);
  }

  requestJoin(
    request: ServerJoinAccessRequest,
    selector?: ServerSourceSelector
  ): Observable<ServerJoinAccessResponse> {
    const actorUserId = this.resolveActorUserId(request.userId, selector);

    return this.api.requestJoin({
      ...request,
      userId: actorUserId,
      userPublicKey: actorUserId
    }, selector);
  }

  createInvite(
    serverId: string,
    request: CreateServerInviteRequest,
    selector?: ServerSourceSelector
  ): Observable<ServerInviteInfo> {
    return this.api.createInvite(serverId, request, selector);
  }

  getInvite(inviteId: string, selector?: ServerSourceSelector): Observable<ServerInviteInfo> {
    return this.api.getInvite(inviteId, selector);
  }

  kickServerMember(
    serverId: string,
    request: KickServerMemberRequest,
    selector?: ServerSourceSelector
  ): Observable<void> {
    return this.api.kickServerMember(serverId, request, selector);
  }

  banServerMember(
    serverId: string,
    request: BanServerMemberRequest,
    selector?: ServerSourceSelector
  ): Observable<void> {
    return this.api.banServerMember(serverId, request, selector);
  }

  unbanServerMember(
    serverId: string,
    request: UnbanServerMemberRequest,
    selector?: ServerSourceSelector
  ): Observable<void> {
    return this.api.unbanServerMember(serverId, request, selector);
  }

  notifyLeave(serverId: string, userId: string, selector?: ServerSourceSelector): Observable<void> {
    return this.api.notifyLeave(serverId, this.resolveActorUserId(userId, selector), selector);
  }

  updateUserCount(serverId: string, count: number): Observable<void> {
    return this.api.updateUserCount(serverId, count);
  }

  sendHeartbeat(serverId: string): Observable<void> {
    return this.api.sendHeartbeat(serverId);
  }

  private loadConnectionSettings(): void {
    const stored = localStorage.getItem(STORAGE_KEY_CONNECTION_SETTINGS);

    if (!stored) {
      this.shouldSearchAllServers = true;
      return;
    }

    try {
      const parsed = JSON.parse(stored) as { searchAllServers?: boolean };

      this.shouldSearchAllServers = parsed.searchAllServers ?? true;
    } catch {
      this.shouldSearchAllServers = true;
    }
  }

  private resolveActorUserId(userId: string, selector?: ServerSourceSelector): string {
    return this.signalServerAuth.resolveActorUserIdForServer(
      this.resolveSelectorServerUrl(selector),
      userId
    );
  }

  private resolveSelectorServerUrl(selector?: ServerSourceSelector): string | undefined {
    const sourceUrl = selector?.sourceUrl?.trim();

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

    const sourceId = selector?.sourceId?.trim();

    if (sourceId) {
      return this.servers().find((endpoint) => endpoint.id === sourceId)?.url;
    }

    return this.activeServer()?.url;
  }
}
