import { Injectable, inject, signal } from '@angular/core';
import { HttpErrorResponse } from '@angular/common/http';
import { catchError, switchMap, tap, throwError } from 'rxjs';
import { Observable } from 'rxjs';
import { AuthTokenPair } from '../models/auth-api.model';
import { AuthError, authErrorCodeFromStatus } from '../models/auth-error.model';
import { AuthApiService } from './auth-api.service';
import { Ed25519KeypairService } from './ed25519-keypair.service';
import { SessionService } from './session.service';

export type LoginPhase = 'idle' | 'requesting-challenge' | 'signing' | 'verifying' | 'done';

/**
 * Orchestrates the Ed25519 challenge/response admin auth flow end to end:
 *
 *   GET  /api/admin/auth/challenge      -> { nonce }
 *   sign(nonce) with local Ed25519 key  -> signature
 *   POST /api/admin/auth/verify         -> { token, refreshToken }
 *
 * This is the lowest-level orchestrator; components should go through
 * AuthFacade rather than calling this directly.
 */
@Injectable({ providedIn: 'root' })
export class AuthService {
  private readonly api = inject(AuthApiService);
  private readonly keypair = inject(Ed25519KeypairService);
  private readonly session = inject(SessionService);

  private readonly loginPhaseSignal = signal<LoginPhase>('idle');
  private readonly lastErrorSignal = signal<AuthError | null>(null);

  readonly loginPhase = this.loginPhaseSignal.asReadonly();
  readonly lastError = this.lastErrorSignal.asReadonly();

  constructor() {
    this.session.onRefreshDue(() => this.refresh().subscribe());
  }

  /** Restores a persisted session on app bootstrap. Call once from an APP_INITIALIZER or root component. */
  restoreSession(): void {
    this.session.restore();
  }

  login(): Observable<AuthTokenPair> {
    this.lastErrorSignal.set(null);
    this.loginPhaseSignal.set('requesting-challenge');

    return this.api.requestChallenge().pipe(
      switchMap(challenge =>
        this.signChallenge(challenge.nonce).pipe(
          switchMap(({ publicKeyBase64, signature }) => {
            this.loginPhaseSignal.set('verifying');
            return this.api.verifySignature({ publicKey: publicKeyBase64, signature, nonce: challenge.nonce });
          })
        )
      ),
      tap(tokens => {
        this.session.activate(tokens);
        this.loginPhaseSignal.set('done');
      }),
      catchError(error => this.handleAuthError<AuthTokenPair>(error, 'invalid-signature'))
    );
  }

  refresh(): Observable<AuthTokenPair> {
    const refreshToken = this.session.getRefreshToken();
    if (!refreshToken) {
      this.session.markExpired();
      return throwError(() => this.toAuthError({ code: 'session-expired', message: 'No refresh token available.' }));
    }

    return this.api.refresh({ refreshToken }).pipe(
      tap(tokens => this.session.activate(tokens)),
      catchError(error => this.handleAuthError<AuthTokenPair>(error, 'session-expired', () => this.session.markExpired()))
    );
  }

  logout(): Observable<void> {
    const refreshToken = this.session.getRefreshToken();
    this.session.clear();
    if (!refreshToken) {
      return new Observable<void>(subscriber => {
        subscriber.next();
        subscriber.complete();
      });
    }
    return this.api.logout(refreshToken).pipe(catchError(() => throwError(() => null)));
  }

  private signChallenge(nonce: string): Observable<{ publicKeyBase64: string; signature: string }> {
    this.loginPhaseSignal.set('signing');
    return new Observable<{ publicKeyBase64: string; signature: string }>(subscriber => {
      this.keypair
        .getOrCreateKeyPair()
        .then(({ publicKeyBase64 }) =>
          this.keypair.sign(nonce).then(signature => {
            subscriber.next({ publicKeyBase64, signature });
            subscriber.complete();
          })
        )
        .catch(error => subscriber.error(error));
    });
  }

  private handleAuthError<T>(error: unknown, fallbackCode: AuthError['code'], onError?: () => void): Observable<T> {
    onError?.();
    return throwError(() => this.toAuthError(this.toAuthErrorShape(error, fallbackCode)));
  }

  private toAuthErrorShape(error: unknown, fallbackCode: AuthError['code']): AuthError {
    if (error instanceof HttpErrorResponse) {
      return { code: authErrorCodeFromStatus(error.status), message: error.message, status: error.status };
    }
    if (error instanceof Error) {
      return { code: fallbackCode, message: error.message };
    }
    return { code: fallbackCode, message: 'Unknown authentication error.' };
  }

  private toAuthError(error: AuthError): AuthError {
    this.lastErrorSignal.set(error);
    return error;
  }
}
