import { Injectable, inject } from '@angular/core';
import { Store } from '@ngrx/store';
import { firstValueFrom } from 'rxjs';
import type { User } from '../../../../shared-kernel';
import { selectCurrentUser } from '../../../../store/users/users.selectors';
import type { LoginResponse } from '../../domain/models/authentication.model';
import type { SignalServerCredential } from '../../domain/models/signal-server-credential.model';
import { ProvisionUsernameCollisionError } from '../../domain/logic/signal-server-provision.rules';
import { type ResolvedSignalIdentity, resolveSignalIdentity } from '../../domain/logic/signal-server-credential-resolution.rules';
import { resolveSelfPresenceUserIds } from '../../domain/logic/self-presence-identity.rules';
import { AuthTokenStoreService } from './auth-token-store.service';
import { ProvisionSecretStoreService, generateProvisionSecret } from './provision-secret-store.service';
import { SignalServerCredentialStoreService } from './signal-server-credential-store.service';
import { SignalServerProvisionerService, type ProvisionResult } from './signal-server-provisioner.service';
import { SignalServerProvisionNoticeService } from './signal-server-provision-notice.service';

export type EnsureProvisionedResult =
  | { kind: 'existing'; credential: SignalServerCredential }
  | { kind: 'provisioned'; result: ProvisionResult }
  | { kind: 'skipped'; reason: 'no-home-user' | 'no-provision-secret' | 'already-valid' }
  | { kind: 'collision'; error: ProvisionUsernameCollisionError };

@Injectable({ providedIn: 'root' })
export class SignalServerAuthService {
  private readonly store = inject(Store);
  private readonly credentialStore = inject(SignalServerCredentialStoreService);
  private readonly authTokenStore = inject(AuthTokenStoreService);
  private readonly provisionSecretStore = inject(ProvisionSecretStoreService);
  private readonly provisioner = inject(SignalServerProvisionerService);
  private readonly provisionNotice = inject(SignalServerProvisionNoticeService);
  private readonly provisionInFlight = new Map<string, Promise<EnsureProvisionedResult>>();

  getCredential(serverUrl: string): SignalServerCredential | null {
    return this.credentialStore.getCredential(serverUrl);
  }

  hasValidCredential(serverUrl: string): boolean {
    return this.credentialStore.hasValidCredential(serverUrl);
  }

  upsertCredentialFromLogin(
    serverUrl: string,
    response: LoginResponse,
    options: { provisioned?: boolean } = {}
  ): SignalServerCredential {
    return this.provisioner.upsertManualCredential(serverUrl, response, options.provisioned ?? false);
  }

  clearCredential(serverUrl: string): void {
    this.credentialStore.clearCredential(serverUrl);
  }

  migrateHomeCredential(user: Pick<User, 'id' | 'username' | 'displayName' | 'homeSignalServerUrl'>): void {
    const homeSignalServerUrl = user.homeSignalServerUrl?.trim();

    if (!homeSignalServerUrl || this.credentialStore.hasValidCredential(homeSignalServerUrl)) {
      return;
    }

    const tokenEntry = this.authTokenStore.getTokenEntry(homeSignalServerUrl);

    if (!tokenEntry) {
      return;
    }

    this.credentialStore.upsertCredential({
      serverUrl: homeSignalServerUrl,
      userId: user.id,
      username: user.username,
      displayName: user.displayName,
      token: tokenEntry.token,
      expiresAt: tokenEntry.expiresAt,
      provisioned: false
    });
  }

  async ensureHomeProvisionSecret(homeUser: Pick<User, 'id'>, existingSecret?: string | null): Promise<string> {
    const stored = existingSecret ?? await this.provisionSecretStore.getSecret(homeUser.id);

    if (stored) {
      return stored;
    }

    const generated = generateProvisionSecret();

    await this.provisionSecretStore.storeSecret(homeUser.id, generated);

    return generated;
  }

  async ensureProvisioned(serverUrl: string, homeUser?: User | null): Promise<EnsureProvisionedResult> {
    const normalizedUrl = this.normalizeServerUrl(serverUrl);
    const existing = this.credentialStore.getCredential(normalizedUrl);

    if (existing) {
      return { kind: 'existing', credential: existing };
    }

    const inFlight = this.provisionInFlight.get(normalizedUrl);

    if (inFlight) {
      return inFlight;
    }

    const provisionPromise = this.runProvision(normalizedUrl, homeUser);

    this.provisionInFlight.set(normalizedUrl, provisionPromise);

    try {
      return await provisionPromise;
    } finally {
      this.provisionInFlight.delete(normalizedUrl);
    }
  }

  resolveActorUserIdForServer(serverUrl: string | undefined, fallbackUserId: string): string {
    if (!serverUrl?.trim()) {
      return fallbackUserId;
    }

    const credential = this.getCredential(serverUrl);

    return credential?.userId ?? fallbackUserId;
  }

  resolveSelfPresenceUserIdsForRoom(
    currentUser: Pick<User, 'id' | 'oderId'> | null | undefined,
    roomSourceUrl: string | undefined
  ): ReadonlySet<string> {
    const homeOderId = currentUser?.oderId || currentUser?.id;

    return resolveSelfPresenceUserIds({
      homeUserId: currentUser?.id,
      homeOderId,
      actorUserId: homeOderId
        ? this.resolveActorUserIdForServer(roomSourceUrl, homeOderId)
        : undefined
    });
  }

  resolveCredentialForSignalUrl(
    signalUrl: string,
    homeUser?: Pick<User, 'id' | 'homeSignalServerUrl' | 'displayName'> | null
  ): ResolvedSignalIdentity | null {
    const httpUrl = signalUrl.replace(/^ws/i, 'http');

    return resolveSignalIdentity(
      this.credentialStore.getCredential(httpUrl),
      this.authTokenStore.getTokenEntry(httpUrl),
      homeUser
    );
  }

  private async runProvision(
    normalizedUrl: string,
    homeUser?: User | null
  ): Promise<EnsureProvisionedResult> {
    const user = homeUser ?? await firstValueFrom(this.store.select(selectCurrentUser));

    if (!user) {
      return { kind: 'skipped', reason: 'no-home-user' };
    }

    const provisionSecret = await this.provisionSecretStore.getSecret(user.id);

    if (!provisionSecret) {
      return { kind: 'skipped', reason: 'no-provision-secret' };
    }

    try {
      const result = await this.provisioner.provisionOnServer({
        serverUrl: normalizedUrl,
        homeUser: user,
        provisionSecret
      });

      if (result.usedSuffix) {
        this.provisionNotice.publish({
          serverName: this.resolveServerDisplayName(normalizedUrl),
          preferredUsername: user.username,
          provisionedUsername: result.username
        });
      }

      return { kind: 'provisioned', result };
    } catch (error) {
      if (error instanceof ProvisionUsernameCollisionError) {
        return { kind: 'collision', error };
      }

      throw error;
    }
  }

  private normalizeServerUrl(serverUrl: string): string {
    return serverUrl.trim().replace(/\/+$/, '');
  }

  private resolveServerDisplayName(serverUrl: string): string {
    try {
      return new URL(serverUrl).hostname;
    } catch {
      return serverUrl;
    }
  }
}
